From ee211b35bc851dfeeb51237a4fa8bcc83ab7def1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 20 Mar 2024 23:28:58 -0400 Subject: [PATCH] Add support for parsing unnamed URL requirements (#2567) ## Summary First piece of https://github.com/astral-sh/uv/issues/313. In order to support unnamed requirements, we need to be able to parse them in `requirements-txt`, which in turn means that we need to introduce a new type that's distinct from `pep508::Requirement`, given that these _aren't_ PEP 508-compatible requirements. Part of: https://github.com/astral-sh/uv/issues/313. --- crates/pep508-rs/src/lib.rs | 581 +++++++++++++++--- crates/pep508-rs/src/verbatim_url.rs | 14 +- crates/requirements-txt/src/lib.rs | 166 +++-- ...nts_txt__test__line-endings-basic.txt.snap | 216 ++++--- ..._test__line-endings-constraints-a.txt.snap | 36 +- ..._test__line-endings-constraints-b.txt.snap | 72 ++- ..._txt__test__line-endings-editable.txt.snap | 80 +-- ...xt__test__line-endings-for-poetry.txt.snap | 140 +++-- ...txt__test__line-endings-include-a.txt.snap | 54 +- ...txt__test__line-endings-include-b.txt.snap | 18 +- ...__line-endings-poetry-with-hashes.txt.snap | 452 +++++++------- ...nts_txt__test__line-endings-small.txt.snap | 72 ++- ...xt__test__line-endings-whitespace.txt.snap | 80 +-- ...quirements_txt__test__parse-basic.txt.snap | 216 ++++--- ...ts_txt__test__parse-constraints-a.txt.snap | 36 +- ...ts_txt__test__parse-constraints-b.txt.snap | 72 ++- ...ments_txt__test__parse-for-poetry.txt.snap | 140 +++-- ...ements_txt__test__parse-include-a.txt.snap | 54 +- ...ements_txt__test__parse-include-b.txt.snap | 18 +- ...t__test__parse-poetry-with-hashes.txt.snap | 452 +++++++------- ...quirements_txt__test__parse-small.txt.snap | 72 ++- ...ts_txt__test__parse-unix-bare-url.txt.snap | 96 +++ ...ments_txt__test__parse-whitespace.txt.snap | 80 +-- ...txt__test__parse-windows-bare-url.txt.snap | 96 +++ .../test-data/requirements-txt/bare-url.txt | 3 + crates/uv-resolver/src/bare.rs | 1 + crates/uv-resolver/src/lib.rs | 3 +- crates/uv-resolver/src/preferences.rs | 21 +- crates/uv/src/requirements.rs | 32 +- crates/uv/tests/pip_compile.rs | 54 +- 30 files changed, 2122 insertions(+), 1305 deletions(-) create mode 100644 crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap create mode 100644 crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap create mode 100644 crates/requirements-txt/test-data/requirements-txt/bare-url.txt create mode 100644 crates/uv-resolver/src/bare.rs diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 89c70c7cc..ca7a0bd05 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -122,23 +122,105 @@ create_exception!( "A PEP 508 parser error with span information" ); -/// A PEP 508 dependency specification +/// A requirement specifier in a `requirements.txt` file. +#[derive(Hash, Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum RequirementsTxtRequirement { + /// A PEP 508-compliant dependency specifier. + Pep508(Requirement), + /// A PEP 508-like, direct URL dependency specifier. + Unnamed(UnnamedRequirement), +} + +impl Display for RequirementsTxtRequirement { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pep508(requirement) => write!(f, "{requirement}"), + Self::Unnamed(requirement) => write!(f, "{requirement}"), + } + } +} + +/// A PEP 508-like, direct URL dependency specifier without a package name. +/// +/// In a `requirements.txt` file, the name of the package is optional for direct URL +/// 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, + /// The list of extras such as `security`, `tests` in + /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. + pub extras: Vec, + /// The markers such as `python_version > "3.8"` in + /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. + /// Those are a nested and/or tree. + pub marker: Option, +} + +impl Display for UnnamedRequirement { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.url)?; + if !self.extras.is_empty() { + write!( + f, + "[{}]", + self.extras + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + )?; + } + if let Some(marker) = &self.marker { + write!(f, " ; {}", marker)?; + } + Ok(()) + } +} + +/// +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for UnnamedRequirement { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + +/// +#[cfg(feature = "serde")] +impl Serialize for UnnamedRequirement { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(self) + } +} + +/// A PEP 508 dependency specifier. #[derive(Hash, Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))] pub struct Requirement { /// The distribution name such as `numpy` in - /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` + /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. pub name: PackageName, /// The list of extras such as `security`, `tests` in - /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` + /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. pub extras: Vec, /// The version specifier such as `>= 2.8.1`, `== 2.8.*` in - /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` + /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. /// or a url pub version_or_url: Option, /// The markers such as `python_version > "3.8"` in /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. - /// Those are a nested and/or tree + /// Those are a nested and/or tree. pub marker: Option, } @@ -377,7 +459,7 @@ impl Requirement { } } - /// Returns whether the markers apply for the given environment + /// Returns whether the markers apply for the given environment. pub fn evaluate_markers_and_report( &self, env: &MarkerEnvironment, @@ -394,16 +476,69 @@ impl Requirement { impl FromStr for Requirement { type Err = Pep508Error; - /// Parse a [Dependency Specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) + /// Parse a [Dependency Specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/). fn from_str(input: &str) -> Result { - parse(&mut Cursor::new(input), None) + parse_pep508_requirement(&mut Cursor::new(input), None) } } impl Requirement { - /// Parse a [Dependency Specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) + /// Parse a [Dependency Specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/). pub fn parse(input: &str, working_dir: impl AsRef) -> Result { - parse(&mut Cursor::new(input), Some(working_dir.as_ref())) + parse_pep508_requirement(&mut Cursor::new(input), Some(working_dir.as_ref())) + } +} + +impl FromStr for UnnamedRequirement { + type Err = Pep508Error; + + /// Parse a PEP 508-like direct URL requirement without a package name. + fn from_str(input: &str) -> Result { + 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) -> Result { + parse_unnamed_requirement(&mut Cursor::new(input), Some(working_dir.as_ref())) + } +} + +impl FromStr for RequirementsTxtRequirement { + type Err = Pep508Error; + + /// Parse a requirement as seen in a `requirements.txt` file. + fn from_str(input: &str) -> Result { + match Requirement::from_str(input) { + Ok(requirement) => Ok(Self::Pep508(requirement)), + Err(err) => match err.message { + Pep508ErrorSource::UnsupportedRequirement(_) => { + Ok(Self::Unnamed(UnnamedRequirement::from_str(input)?)) + } + _ => Err(err), + }, + } + } +} + +impl RequirementsTxtRequirement { + /// Parse a requirement as seen in a `requirements.txt` file. + pub fn parse(input: &str, working_dir: impl AsRef) -> Result { + // Attempt to parse as a PEP 508-compliant requirement. + match Requirement::parse(input, &working_dir) { + Ok(requirement) => Ok(Self::Pep508(requirement)), + Err(err) => match err.message { + Pep508ErrorSource::UnsupportedRequirement(_) => { + // If that fails, attempt to parse as a direct URL requirement. + Ok(Self::Unnamed(UnnamedRequirement::parse( + input, + &working_dir, + )?)) + } + _ => Err(err), + }, + } } } @@ -575,7 +710,7 @@ fn parse_name(cursor: &mut Cursor) -> Result { // Check if the user added a filesystem path without a package name. pip supports this // in `requirements.txt`, but it doesn't adhere to the PEP 508 grammar. let mut clone = cursor.clone().at(start); - return if looks_like_file_path(&mut clone) { + return if looks_like_unnamed_requirement(&mut clone) { Err(Pep508Error { 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()), start, @@ -627,6 +762,37 @@ fn parse_name(cursor: &mut Cursor) -> Result { } } +/// Parse a potential URL from the [`Cursor`], advancing the [`Cursor`] to the end of the URL. +/// +/// Returns `true` if the URL appears to be a viable unnamed requirement, and `false` otherwise. +fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool { + // Read the entire path. + let (start, len) = cursor.take_while(|char| !char.is_whitespace()); + let url = cursor.slice(start, len); + + // Expand any environment variables in the path. + let expanded = expand_env_vars(url); + + // Analyze the path. + let mut chars = expanded.chars(); + + let Some(first_char) = chars.next() else { + return false; + }; + + // Ex) `/bin/ls` + if first_char == '\\' || first_char == '/' || first_char == '.' { + return true; + } + + // Ex) `https://` or `C:` + if split_scheme(&expanded).is_some() { + return true; + } + + false +} + /// parses extras in the `[extra1,extra2] format` fn parse_extras(cursor: &mut Cursor) -> Result, Pep508Error> { let Some(bracket_pos) = cursor.eat_char('[') else { @@ -767,35 +933,6 @@ fn parse_url(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result bool { - let Some((_, first_char)) = cursor.next() else { - return false; - }; - - // Ex) `/bin/ls` - if first_char == '\\' || first_char == '/' || first_char == '.' { - // Read until the end of the path. - cursor.take_while(|char| !char.is_whitespace()); - return true; - } - - // Ex) `C:` - if first_char.is_alphabetic() { - if let Some((_, second_char)) = cursor.next() { - if second_char == ':' { - // Read until the end of the path. - cursor.take_while(|char| !char.is_whitespace()); - return true; - } - } - } - - false -} - /// Create a `VerbatimUrl` to represent the requirement. fn preprocess_url( url: &str, @@ -882,6 +1019,172 @@ fn preprocess_url( } } +/// Like [`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), Pep508Error> { + // wsp* + cursor.eat_whitespace(); + // + 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) +} + +/// 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), Pep508Error> { + // 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(&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/...` or `file:../editable/`. + Some(Scheme::File) => { + let path = path.strip_prefix("//").unwrap_or(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::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::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` + _ => { + #[cfg(feature = "non-pep508-extensions")] + 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/` + #[cfg(feature = "non-pep508-extensions")] + 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)) + } +} + +/// Identify the extras in a relative URL (e.g., `../editable[dev]`). +/// +/// Pip uses `m = re.match(r'^(.+)(\[[^]]+])$', path)`. Our strategy is: +/// - If the string ends with a closing bracket (`]`)... +/// - Iterate backwards until you find the open bracket (`[`)... +/// - But abort if you find another closing bracket (`]`) first. +pub fn split_extras(given: &str) -> Option<(&str, &str)> { + let mut chars = given.char_indices().rev(); + + // If the string ends with a closing bracket (`]`)... + if !matches!(chars.next(), Some((_, ']'))) { + return None; + } + + // Iterate backwards until you find the open bracket (`[`)... + let (index, _) = chars + .take_while(|(_, c)| *c != ']') + .find(|(_, c)| *c == '[')?; + + Some(given.split_at(index)) +} + /// PEP 440 wrapper fn parse_specifier( cursor: &mut Cursor, @@ -973,8 +1276,11 @@ fn parse_version_specifier_parentheses( Ok(requirement_kind) } -/// Parse a [dependency specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers) -fn parse(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result { +/// Parse a PEP 508-compliant [dependency specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers). +fn parse_pep508_requirement( + cursor: &mut Cursor, + working_dir: Option<&Path>, +) -> Result { let start = cursor.pos(); // Technically, the grammar is: @@ -1088,6 +1394,64 @@ fn parse(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result, +) -> Result { + 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(marker::parse_markers_impl(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, + }) +} + /// A library for [dependency specifiers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) /// as originally specified in [PEP 508](https://peps.python.org/pep-0508/) /// @@ -1128,12 +1492,16 @@ mod tests { parse_markers_impl, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue, MarkerValueString, MarkerValueVersion, }; - use crate::{Cursor, Pep508Error, Requirement, VerbatimUrl, VersionOrUrl}; + use crate::{Cursor, Pep508Error, Requirement, UnnamedRequirement, VerbatimUrl, VersionOrUrl}; - fn parse_err(input: &str) -> String { + fn parse_pepe508_err(input: &str) -> String { Requirement::from_str(input).unwrap_err().to_string() } + fn parse_unnamed_err(input: &str) -> String { + UnnamedRequirement::from_str(input).unwrap_err().to_string() + } + #[cfg(windows)] #[test] fn test_preprocess_url_windows() { @@ -1155,7 +1523,7 @@ mod tests { #[test] fn error_empty() { assert_snapshot!( - parse_err(""), + parse_pepe508_err(""), @r" Empty field is not allowed for PEP508 @@ -1166,7 +1534,7 @@ mod tests { #[test] fn error_start() { assert_snapshot!( - parse_err("_name"), + parse_pepe508_err("_name"), @" Expected package name starting with an alphanumeric character, found '_' _name @@ -1177,7 +1545,7 @@ mod tests { #[test] fn error_end() { assert_snapshot!( - parse_err("name_"), + parse_pepe508_err("name_"), @" Package name must end with an alphanumeric character, not '_' name_ @@ -1245,10 +1613,43 @@ mod tests { assert_eq!(numpy.name.as_ref(), "numpy"); } + #[test] + fn direct_url_no_extras() { + let numpy = UnnamedRequirement::from_str("https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl").unwrap(); + assert_eq!(numpy.url.to_string(), "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl"); + assert_eq!(numpy.extras, vec![]); + } + + #[test] + #[cfg(unix)] + fn direct_url_extras() { + let numpy = + UnnamedRequirement::from_str("/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]") + .unwrap(); + assert_eq!( + numpy.url.to_string(), + "file:///path/to/numpy-1.26.4-cp312-cp312-win32.whl" + ); + assert_eq!(numpy.extras, vec![ExtraName::from_str("dev").unwrap()]); + } + + #[test] + #[cfg(windows)] + fn direct_url_extras() { + let numpy = + UnnamedRequirement::from_str("C:\\path\\to\\numpy-1.26.4-cp312-cp312-win32.whl[dev]") + .unwrap(); + assert_eq!( + numpy.url.to_string(), + "file:///C:/path/to/numpy-1.26.4-cp312-cp312-win32.whl" + ); + assert_eq!(numpy.extras, vec![ExtraName::from_str("dev").unwrap()]); + } + #[test] fn error_extras_eof1() { assert_snapshot!( - parse_err("black["), + parse_pepe508_err("black["), @" Missing closing bracket (expected ']', found end of dependency specification) black[ @@ -1259,7 +1660,7 @@ mod tests { #[test] fn error_extras_eof2() { assert_snapshot!( - parse_err("black[d"), + parse_pepe508_err("black[d"), @" Missing closing bracket (expected ']', found end of dependency specification) black[d @@ -1270,7 +1671,7 @@ mod tests { #[test] fn error_extras_eof3() { assert_snapshot!( - parse_err("black[d,"), + parse_pepe508_err("black[d,"), @" Missing closing bracket (expected ']', found end of dependency specification) black[d, @@ -1281,7 +1682,7 @@ mod tests { #[test] fn error_extras_illegal_start1() { assert_snapshot!( - parse_err("black[ö]"), + parse_pepe508_err("black[ö]"), @" Expected an alphanumeric character starting the extra name, found 'ö' black[ö] @@ -1292,7 +1693,7 @@ mod tests { #[test] fn error_extras_illegal_start2() { assert_snapshot!( - parse_err("black[_d]"), + parse_pepe508_err("black[_d]"), @" Expected an alphanumeric character starting the extra name, found '_' black[_d] @@ -1303,7 +1704,7 @@ mod tests { #[test] fn error_extras_illegal_start3() { assert_snapshot!( - parse_err("black[,]"), + parse_pepe508_err("black[,]"), @" Expected either alphanumerical character (starting the extra name) or ']' (ending the extras section), found ',' black[,] @@ -1314,7 +1715,7 @@ mod tests { #[test] fn error_extras_illegal_character() { assert_snapshot!( - parse_err("black[jüpyter]"), + parse_pepe508_err("black[jüpyter]"), @" Invalid character in extras name, expected an alphanumeric character, '-', '_', '.', ',' or ']', found 'ü' black[jüpyter] @@ -1355,7 +1756,7 @@ mod tests { #[test] fn error_extra_with_trailing_comma() { assert_snapshot!( - parse_err("black[d,]"), + parse_pepe508_err("black[d,]"), @" Expected an alphanumeric character starting the extra name, found ']' black[d,] @@ -1366,7 +1767,7 @@ mod tests { #[test] fn error_parenthesized_pep440() { assert_snapshot!( - parse_err("numpy ( ><1.19 )"), + parse_pepe508_err("numpy ( ><1.19 )"), @" no such comparison operator \"><\", must be one of ~= == != <= >= < > === numpy ( ><1.19 ) @@ -1377,7 +1778,7 @@ mod tests { #[test] fn error_parenthesized_parenthesis() { assert_snapshot!( - parse_err("numpy ( >=1.19"), + parse_pepe508_err("numpy ( >=1.19"), @" Missing closing parenthesis (expected ')', found end of dependency specification) numpy ( >=1.19 @@ -1388,7 +1789,7 @@ mod tests { #[test] fn error_whats_that() { assert_snapshot!( - parse_err("numpy % 1.16"), + parse_pepe508_err("numpy % 1.16"), @" Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `%` numpy % 1.16 @@ -1454,7 +1855,7 @@ mod tests { #[test] fn error_marker_incomplete1() { assert_snapshot!( - parse_err(r"numpy; sys_platform"), + parse_pepe508_err(r"numpy; sys_platform"), @" Expected a valid marker operator (such as '>=' or 'not in'), found '' numpy; sys_platform @@ -1465,7 +1866,7 @@ mod tests { #[test] fn error_marker_incomplete2() { assert_snapshot!( - parse_err(r"numpy; sys_platform =="), + parse_pepe508_err(r"numpy; sys_platform =="), @r" Expected marker value, found end of dependency specification numpy; sys_platform == @@ -1476,7 +1877,7 @@ mod tests { #[test] fn error_marker_incomplete3() { assert_snapshot!( - parse_err(r#"numpy; sys_platform == "win32" or"#), + parse_pepe508_err(r#"numpy; sys_platform == "win32" or"#), @r#" Expected marker value, found end of dependency specification numpy; sys_platform == "win32" or @@ -1487,7 +1888,7 @@ mod tests { #[test] fn error_marker_incomplete4() { assert_snapshot!( - parse_err(r#"numpy; sys_platform == "win32" or (os_name == "linux""#), + parse_pepe508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux""#), @r#" Expected ')', found end of dependency specification numpy; sys_platform == "win32" or (os_name == "linux" @@ -1498,7 +1899,7 @@ mod tests { #[test] fn error_marker_incomplete5() { assert_snapshot!( - parse_err(r#"numpy; sys_platform == "win32" or (os_name == "linux" and"#), + parse_pepe508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux" and"#), @r#" Expected marker value, found end of dependency specification numpy; sys_platform == "win32" or (os_name == "linux" and @@ -1509,7 +1910,7 @@ mod tests { #[test] fn error_pep440() { assert_snapshot!( - parse_err(r"numpy >=1.1.*"), + parse_pepe508_err(r"numpy >=1.1.*"), @r" Operator >= cannot be used with a wildcard version specifier numpy >=1.1.* @@ -1520,7 +1921,7 @@ mod tests { #[test] fn error_no_name() { assert_snapshot!( - parse_err(r"==0.0"), + parse_pepe508_err(r"==0.0"), @r" Expected package name starting with an alphanumeric character, found '=' ==0.0 @@ -1530,9 +1931,9 @@ mod tests { } #[test] - fn error_bare_url() { + fn error_unnamedunnamed_url() { assert_snapshot!( - parse_err(r"git+https://github.com/pallets/flask.git"), + parse_pepe508_err(r"git+https://github.com/pallets/flask.git"), @" URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`). git+https://github.com/pallets/flask.git @@ -1541,9 +1942,9 @@ mod tests { } #[test] - fn error_bare_file_path() { + fn error_unnamed_file_path() { assert_snapshot!( - parse_err(r"/path/to/flask.tar.gz"), + parse_pepe508_err(r"/path/to/flask.tar.gz"), @r###" 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`). /path/to/flask.tar.gz @@ -1555,7 +1956,7 @@ mod tests { #[test] fn error_no_comma_between_extras() { assert_snapshot!( - parse_err(r"name[bar baz]"), + parse_pepe508_err(r"name[bar baz]"), @" Expected either ',' (separating extras) or ']' (ending the extras section), found 'b' name[bar baz] @@ -1566,7 +1967,7 @@ mod tests { #[test] fn error_extra_comma_after_extras() { assert_snapshot!( - parse_err(r"name[bar, baz,]"), + parse_pepe508_err(r"name[bar, baz,]"), @" Expected an alphanumeric character starting the extra name, found ']' name[bar, baz,] @@ -1577,7 +1978,7 @@ mod tests { #[test] fn error_extras_not_closed() { assert_snapshot!( - parse_err(r"name[bar, baz >= 1.0"), + parse_pepe508_err(r"name[bar, baz >= 1.0"), @" Expected either ',' (separating extras) or ']' (ending the extras section), found '>' name[bar, baz >= 1.0 @@ -1588,7 +1989,7 @@ mod tests { #[test] fn error_no_space_after_url() { assert_snapshot!( - parse_err(r"name @ https://example.com/; extra == 'example'"), + parse_pepe508_err(r"name @ https://example.com/; extra == 'example'"), @" Missing space before ';', the end of the URL is ambiguous name @ https://example.com/; extra == 'example' @@ -1599,7 +2000,7 @@ mod tests { #[test] fn error_name_at_nothing() { assert_snapshot!( - parse_err(r"name @"), + parse_pepe508_err(r"name @"), @" Expected URL name @ @@ -1610,7 +2011,7 @@ mod tests { #[test] fn test_error_invalid_marker_key() { assert_snapshot!( - parse_err(r"name; invalid_name"), + parse_pepe508_err(r"name; invalid_name"), @" Expected a valid marker name, found 'invalid_name' name; invalid_name @@ -1621,7 +2022,7 @@ mod tests { #[test] fn error_markers_invalid_order() { assert_snapshot!( - parse_err("name; '3.7' <= invalid_name"), + parse_pepe508_err("name; '3.7' <= invalid_name"), @" Expected a valid marker name, found 'invalid_name' name; '3.7' <= invalid_name @@ -1632,7 +2033,7 @@ mod tests { #[test] fn error_markers_notin() { assert_snapshot!( - parse_err("name; '3.7' notin python_version"), + parse_pepe508_err("name; '3.7' notin python_version"), @" Expected a valid marker operator (such as '>=' or 'not in'), found 'notin' name; '3.7' notin python_version @@ -1643,7 +2044,7 @@ mod tests { #[test] fn error_markers_inpython_version() { assert_snapshot!( - parse_err("name; '3.6'inpython_version"), + parse_pepe508_err("name; '3.6'inpython_version"), @" Expected a valid marker operator (such as '>=' or 'not in'), found 'inpython_version' name; '3.6'inpython_version @@ -1654,7 +2055,7 @@ mod tests { #[test] fn error_markers_not_python_version() { assert_snapshot!( - parse_err("name; '3.7' not python_version"), + parse_pepe508_err("name; '3.7' not python_version"), @" Expected 'i', found 'p' name; '3.7' not python_version @@ -1665,7 +2066,7 @@ mod tests { #[test] fn error_markers_invalid_operator() { assert_snapshot!( - parse_err("name; '3.7' ~ python_version"), + parse_pepe508_err("name; '3.7' ~ python_version"), @" Expected a valid marker operator (such as '>=' or 'not in'), found '~' name; '3.7' ~ python_version @@ -1676,7 +2077,7 @@ mod tests { #[test] fn error_invalid_prerelease() { assert_snapshot!( - parse_err("name==1.0.org1"), + parse_pepe508_err("name==1.0.org1"), @" after parsing 1.0, found \".org1\" after it, which is not part of a valid version name==1.0.org1 @@ -1687,7 +2088,7 @@ mod tests { #[test] fn error_no_version_value() { assert_snapshot!( - parse_err("name=="), + parse_pepe508_err("name=="), @" Unexpected end of version specifier, expected version name== @@ -1698,7 +2099,7 @@ mod tests { #[test] fn error_no_version_operator() { assert_snapshot!( - parse_err("name 1.0"), + parse_pepe508_err("name 1.0"), @" Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `1` name 1.0 @@ -1709,7 +2110,7 @@ mod tests { #[test] fn error_random_char() { assert_snapshot!( - parse_err("name >= 1.0 #"), + parse_pepe508_err("name >= 1.0 #"), @" Trailing `#` is not allowed name >= 1.0 # @@ -1717,6 +2118,18 @@ mod tests { ); } + #[test] + fn error_invalid_extra_unnamed_url() { + assert_snapshot!( + parse_unnamed_err("/foo-3.0.0-py3-none-any.whl[d,]"), + @r###" + Expected an alphanumeric character starting the extra name, found ']' + /foo-3.0.0-py3-none-any.whl[d,] + ^ + "### + ); + } + /// Check that the relative path support feature toggle works. #[test] fn non_pep508_paths() { diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index 62f3907aa..48d9e684d 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -46,7 +46,8 @@ impl VerbatimUrl { let (path, fragment) = split_fragment(&path); // Convert to a URL. - let mut url = Url::from_file_path(path).expect("path is absolute"); + let mut url = Url::from_file_path(path.clone()) + .unwrap_or_else(|_| panic!("path is absolute: {}", path.display())); // Set the fragment, if it exists. if let Some(fragment) = fragment { @@ -81,7 +82,13 @@ impl VerbatimUrl { let (path, fragment) = split_fragment(&path); // Convert to a URL. - let mut url = Url::from_file_path(path).expect("path is absolute"); + let mut url = Url::from_file_path(path.clone()).unwrap_or_else(|_| { + panic!( + "path is absolute: {}, {}", + path.display(), + working_dir.as_ref().display() + ) + }); // Set the fragment, if it exists. if let Some(fragment) = fragment { @@ -109,7 +116,8 @@ impl VerbatimUrl { let (path, fragment) = split_fragment(&path); // Convert to a URL. - let mut url = Url::from_file_path(path).expect("path is absolute"); + let mut url = Url::from_file_path(path.clone()) + .unwrap_or_else(|_| panic!("path is absolute: {}", path.display())); // Set the fragment, if it exists. if let Some(fragment) = fragment { diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 6d6d44bb4..cbaf73cd4 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -46,8 +46,8 @@ use unscanny::{Pattern, Scanner}; use url::Url; use pep508_rs::{ - expand_env_vars, split_scheme, Extras, Pep508Error, Pep508ErrorSource, Requirement, Scheme, - VerbatimUrl, + expand_env_vars, split_scheme, Extras, Pep508Error, Pep508ErrorSource, Requirement, + RequirementsTxtRequirement, Scheme, VerbatimUrl, }; use uv_client::Connectivity; use uv_fs::{normalize_url_path, Simplified}; @@ -287,7 +287,7 @@ impl Display for EditableRequirement { #[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)] pub struct RequirementEntry { /// The actual PEP 508 requirement - pub requirement: Requirement, + pub requirement: RequirementsTxtRequirement, /// Hashes of the downloadable packages pub hashes: Vec, /// Editable installation, see e.g. @@ -470,12 +470,19 @@ impl RequirementsTxt { // Treat any nested requirements or constraints as constraints. This differs // from `pip`, which seems to treat `-r` requirements in constraints files as // _requirements_, but we don't want to support that. - data.constraints.extend( - sub_constraints - .requirements - .into_iter() - .map(|requirement_entry| requirement_entry.requirement), - ); + for entry in sub_constraints.requirements { + match entry.requirement { + RequirementsTxtRequirement::Pep508(requirement) => { + data.constraints.push(requirement); + } + RequirementsTxtRequirement::Unnamed(_) => { + return Err(RequirementsTxtParserError::UnnamedConstraint { + start, + end, + }); + } + } + } data.constraints.extend(sub_constraints.constraints); } RequirementsTxtStatement::RequirementEntry(requirement_entry) => { @@ -610,7 +617,7 @@ fn parse_entry( } })?; RequirementsTxtStatement::FindLinks(path_or_url) - } else if s.at(char::is_ascii_alphanumeric) { + } else if s.at(char::is_ascii_alphanumeric) || s.at(|char| matches!(char, '.' | '/' | '$')) { let (requirement, hashes) = parse_requirement_and_hashes(s, content, working_dir)?; RequirementsTxtStatement::RequirementEntry(RequirementEntry { requirement, @@ -675,7 +682,7 @@ fn parse_requirement_and_hashes( s: &mut Scanner, content: &str, working_dir: &Path, -) -> Result<(Requirement, Vec), RequirementsTxtParserError> { +) -> Result<(RequirementsTxtRequirement, Vec), RequirementsTxtParserError> { // PEP 508 requirement let start = s.cursor(); // Termination: s.eat() eventually becomes None @@ -731,41 +738,26 @@ fn parse_requirement_and_hashes( } } - // If the requirement looks like an editable requirement (with a missing `-e`), raise an - // error. - // - // Slashes are not allowed in package names, so these would be rejected in the next step anyway. - if requirement.contains('/') || requirement.contains('\\') { - let path = Path::new(requirement); - let path = if path.is_absolute() { - Cow::Borrowed(path) - } else { - Cow::Owned(working_dir.join(path)) - }; - if path.is_dir() { - return Err(RequirementsTxtParserError::MissingEditablePrefix( - requirement.to_string(), - )); - } - } - let requirement = - Requirement::parse(requirement, working_dir).map_err(|err| match err.message { - Pep508ErrorSource::String(_) | Pep508ErrorSource::UrlError(_) => { - RequirementsTxtParserError::Pep508 { - source: err, - start, - end, + RequirementsTxtRequirement::parse(requirement, working_dir).map_err(|err| { + match err.message { + Pep508ErrorSource::String(_) | Pep508ErrorSource::UrlError(_) => { + RequirementsTxtParserError::Pep508 { + source: err, + start, + end, + } } - } - Pep508ErrorSource::UnsupportedRequirement(_) => { - RequirementsTxtParserError::UnsupportedRequirement { - source: err, - start, - end, + Pep508ErrorSource::UnsupportedRequirement(_) => { + RequirementsTxtParserError::UnsupportedRequirement { + source: err, + start, + end, + } } } })?; + let hashes = if has_hashes { let hashes = parse_hashes(content, s)?; eat_trailing_line(content, s)?; @@ -863,7 +855,10 @@ pub enum RequirementsTxtParserError { InvalidEditablePath(String), UnsupportedUrl(String), MissingRequirementPrefix(String), - MissingEditablePrefix(String), + UnnamedConstraint { + start: usize, + end: usize, + }, Parser { message: String, line: usize, @@ -911,7 +906,10 @@ impl RequirementsTxtParserError { }, Self::UnsupportedUrl(url) => Self::UnsupportedUrl(url), Self::MissingRequirementPrefix(given) => Self::MissingRequirementPrefix(given), - Self::MissingEditablePrefix(given) => Self::MissingEditablePrefix(given), + Self::UnnamedConstraint { start, end } => Self::UnnamedConstraint { + start: start + offset, + end: end + offset, + }, Self::Parser { message, line, @@ -959,11 +957,8 @@ impl Display for RequirementsTxtParserError { Self::MissingRequirementPrefix(given) => { write!(f, "Requirement `{given}` looks like a requirements file but was passed as a package name. Did you mean `-r {given}`?") } - Self::MissingEditablePrefix(given) => { - write!( - f, - "Requirement `{given}` looks like a directory but was passed as a package name. Did you mean `-e {given}`?" - ) + Self::UnnamedConstraint { .. } => { + write!(f, "Unnamed requirements are not allowed as constraints") } Self::Parser { message, @@ -1004,7 +999,7 @@ impl std::error::Error for RequirementsTxtParserError { Self::InvalidEditablePath(_) => None, Self::UnsupportedUrl(_) => None, Self::MissingRequirementPrefix(_) => None, - Self::MissingEditablePrefix(_) => None, + Self::UnnamedConstraint { .. } => None, Self::UnsupportedRequirement { source, .. } => Some(source), Self::Pep508 { source, .. } => Some(source), Self::Subfile { source, .. } => Some(source.as_ref()), @@ -1048,10 +1043,10 @@ impl Display for RequirementsTxtFileError { self.file.user_display(), ) } - RequirementsTxtParserError::MissingEditablePrefix(given) => { + RequirementsTxtParserError::UnnamedConstraint { .. } => { write!( f, - "Requirement `{given}` in `{}` looks like a directory but was passed as a package name. Did you mean `-e {given}`?", + "Unnamed requirements are not allowed as constraints in `{}`", self.file.user_display(), ) } @@ -1177,14 +1172,14 @@ mod test { use tempfile::tempdir; use test_case::test_case; use unscanny::Scanner; - use uv_client::Connectivity; + use uv_client::Connectivity; use uv_fs::Simplified; use crate::{calculate_row_column, EditableRequirement, RequirementsTxt}; fn workspace_test_data_dir() -> PathBuf { - PathBuf::from("./test-data") + PathBuf::from("./test-data").canonicalize().unwrap() } #[test_case(Path::new("basic.txt"))] @@ -1254,6 +1249,53 @@ mod test { insta::assert_debug_snapshot!(snapshot, actual); } + #[cfg(unix)] + #[test_case(Path::new("bare-url.txt"))] + #[tokio::test] + async fn parse_unnamed_unix(path: &Path) { + let working_dir = workspace_test_data_dir().join("requirements-txt"); + let requirements_txt = working_dir.join(path); + + let actual = RequirementsTxt::parse(requirements_txt, &working_dir, Connectivity::Offline) + .await + .unwrap(); + + let snapshot = format!("parse-unix-{}", path.to_string_lossy()); + let pattern = regex::escape(&working_dir.simplified_display().to_string()); + let filters = vec![(pattern.as_str(), "[WORKSPACE_DIR]")]; + insta::with_settings!({ + filters => filters + }, { + insta::assert_debug_snapshot!(snapshot, actual); + }); + } + + #[cfg(windows)] + #[test_case(Path::new("bare-url.txt"))] + #[tokio::test] + async fn parse_unnamed_windows(path: &Path) { + let working_dir = workspace_test_data_dir().join("requirements-txt"); + let requirements_txt = working_dir.join(path); + + let actual = RequirementsTxt::parse(requirements_txt, &working_dir, Connectivity::Offline) + .await + .unwrap(); + + let snapshot = format!("parse-windows-{}", path.to_string_lossy()); + let pattern = regex::escape( + &working_dir + .simplified_display() + .to_string() + .replace('\\', "/"), + ); + let filters = vec![(pattern.as_str(), "[WORKSPACE_DIR]")]; + insta::with_settings!({ + filters => filters + }, { + insta::assert_debug_snapshot!(snapshot, actual); + }); + } + #[tokio::test] async fn invalid_include_missing_file() -> Result<()> { let temp_dir = assert_fs::TempDir::new()?; @@ -1566,14 +1608,16 @@ mod test { RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "flask", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "flask", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap index 30b4fb1f6..cba9974a9 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap @@ -5,140 +5,152 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.24.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.24.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2.0.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.0.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "python-dateutil", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2.8.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "python-dateutil", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.8.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pytz", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2023.3", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "pytz", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2023.3", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "six", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.16.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "six", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.16.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "tzdata", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2023.3", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "tzdata", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2023.3", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap index dc4a4806e..e6ab4fe5a 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap @@ -5,25 +5,27 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "django-debug-toolbar", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: LessThan, - version: "2.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "django-debug-toolbar", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: LessThan, + version: "2.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap index 91961fdad..99d4e0cfb 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap @@ -5,48 +5,52 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "django", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2.1.15", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "django", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.1.15", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pytz", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2023.3", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "pytz", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2023.3", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap index c6f1b96ab..80ebe3f58 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap @@ -5,53 +5,57 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [ - ExtraName( - "tabulate", + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", ), - ], - version_or_url: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", + extras: [ + ExtraName( + "tabulate", + ), + ], + version_or_url: Some( + Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + given: Some( + "https://github.com/pandas-dev/pandas", ), - port: None, - path: "/pandas-dev/pandas", - query: None, - fragment: None, }, - given: Some( - "https://github.com/pandas-dev/pandas", - ), - }, + ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap index e0354af92..b8d33f818 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap @@ -5,91 +5,99 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "inflection", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "0.5.1", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "inflection", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "0.5.1", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "upsidedown", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "0.4", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "upsidedown", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "0.4", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [ - ExtraName( - "tabulate", + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", ), - ], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "1", - }, - VersionSpecifier { - operator: LessThan, - version: "2", - }, - ], + extras: [ + ExtraName( + "tabulate", + ), + ], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "1", + }, + VersionSpecifier { + operator: LessThan, + version: "2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap index 2dc8c591f..54a870e38 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap @@ -5,37 +5,41 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "tomli", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "tomli", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.24.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.24.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap index 3bdafcf32..bf1b07b15 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap @@ -5,14 +5,16 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "tomli", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "tomli", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap index c570919d3..0aacf9a39 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap @@ -5,219 +5,227 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "werkzeug", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "werkzeug", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.2.3", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "2.2.3", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4.0", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4.0", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", ], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "urllib3", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "urllib3", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.26.15", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "1.26.15", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", ], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "ansicon", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "ansicon", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.89.0", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "1.89.0", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvString( + PlatformSystem, + ), + operator: Equal, + r_value: QuotedString( + "Windows", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvString( - PlatformSystem, - ), - operator: Equal, - r_value: QuotedString( - "Windows", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", ], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "requests-oauthlib", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "requests-oauthlib", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.3.1", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "1.3.1", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4.0", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4.0", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", @@ -225,52 +233,54 @@ RequirementsTxt { editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "psycopg2", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "psycopg2", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.9.5", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "2.9.5", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4.0", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4.0", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955", "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa", diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap index 60b773732..00d71b0de 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap @@ -5,48 +5,52 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "tqdm", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "4.65.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "tqdm", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "4.65.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "tomli-w", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.0.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "tomli-w", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.0.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap index c6f1b96ab..80ebe3f58 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap @@ -5,53 +5,57 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [ - ExtraName( - "tabulate", + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", ), - ], - version_or_url: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", + extras: [ + ExtraName( + "tabulate", + ), + ], + version_or_url: Some( + Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + given: Some( + "https://github.com/pandas-dev/pandas", ), - port: None, - path: "/pandas-dev/pandas", - query: None, - fragment: None, }, - given: Some( - "https://github.com/pandas-dev/pandas", - ), - }, + ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap index 30b4fb1f6..cba9974a9 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap @@ -5,140 +5,152 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.24.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.24.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2.0.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.0.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "python-dateutil", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2.8.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "python-dateutil", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.8.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pytz", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2023.3", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "pytz", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2023.3", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "six", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.16.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "six", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.16.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "tzdata", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2023.3", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "tzdata", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2023.3", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap index dc4a4806e..e6ab4fe5a 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap @@ -5,25 +5,27 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "django-debug-toolbar", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: LessThan, - version: "2.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "django-debug-toolbar", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: LessThan, + version: "2.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap index 91961fdad..99d4e0cfb 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap @@ -5,48 +5,52 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "django", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2.1.15", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "django", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.1.15", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pytz", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "2023.3", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "pytz", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2023.3", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap index e0354af92..b8d33f818 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap @@ -5,91 +5,99 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "inflection", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "0.5.1", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "inflection", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "0.5.1", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "upsidedown", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "0.4", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "upsidedown", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "0.4", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [ - ExtraName( - "tabulate", + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", ), - ], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "1", - }, - VersionSpecifier { - operator: LessThan, - version: "2", - }, - ], + extras: [ + ExtraName( + "tabulate", + ), + ], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "1", + }, + VersionSpecifier { + operator: LessThan, + version: "2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap index 2dc8c591f..54a870e38 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap @@ -5,37 +5,41 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "tomli", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "tomli", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.24.2", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.24.2", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap index 3bdafcf32..bf1b07b15 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap @@ -5,14 +5,16 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "tomli", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "tomli", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap index c570919d3..0aacf9a39 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap @@ -5,219 +5,227 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "werkzeug", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "werkzeug", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.2.3", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "2.2.3", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4.0", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4.0", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", ], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "urllib3", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "urllib3", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.26.15", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "1.26.15", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", ], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "ansicon", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "ansicon", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.89.0", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "1.89.0", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvString( + PlatformSystem, + ), + operator: Equal, + r_value: QuotedString( + "Windows", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvString( - PlatformSystem, - ), - operator: Equal, - r_value: QuotedString( - "Windows", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", ], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "requests-oauthlib", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "requests-oauthlib", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.3.1", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "1.3.1", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4.0", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4.0", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", @@ -225,52 +233,54 @@ RequirementsTxt { editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "psycopg2", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( + requirement: Pep508( + Requirement { + name: PackageName( + "psycopg2", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "2.9.5", + }, + ], + ), + ), + ), + marker: Some( + And( [ - VersionSpecifier { - operator: Equal, - version: "2.9.5", - }, + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: GreaterEqual, + r_value: QuotedString( + "3.8", + ), + }, + ), + Expression( + MarkerExpression { + l_value: MarkerEnvVersion( + PythonVersion, + ), + operator: LessThan, + r_value: QuotedString( + "4.0", + ), + }, + ), ], ), ), - ), - marker: Some( - And( - [ - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: GreaterEqual, - r_value: QuotedString( - "3.8", - ), - }, - ), - Expression( - MarkerExpression { - l_value: MarkerEnvVersion( - PythonVersion, - ), - operator: LessThan, - r_value: QuotedString( - "4.0", - ), - }, - ), - ], - ), - ), - }, + }, + ), hashes: [ "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955", "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa", diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap index 60b773732..00d71b0de 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap @@ -5,48 +5,52 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "tqdm", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "4.65.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "tqdm", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "4.65.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "tomli-w", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: Equal, - version: "1.0.0", - }, - ], + requirement: Pep508( + Requirement { + name: PackageName( + "tomli-w", + ), + extras: [], + version_or_url: Some( + VersionSpecifier( + VersionSpecifiers( + [ + VersionSpecifier { + operator: Equal, + version: "1.0.0", + }, + ], + ), ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap new file mode 100644 index 000000000..2d3470ce4 --- /dev/null +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap @@ -0,0 +1,96 @@ +--- +source: crates/requirements-txt/src/lib.rs +expression: actual +--- +RequirementsTxt { + requirements: [ + RequirementEntry { + requirement: Unnamed( + UnnamedRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "[WORKSPACE_DIR]/scripts/editable-installs/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/editable-installs/black_editable", + ), + }, + extras: [], + marker: None, + }, + ), + hashes: [], + editable: false, + }, + RequirementEntry { + requirement: Unnamed( + UnnamedRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "[WORKSPACE_DIR]/scripts/editable-installs/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/editable-installs/black_editable", + ), + }, + extras: [ + ExtraName( + "dev", + ), + ], + marker: None, + }, + ), + hashes: [], + editable: false, + }, + RequirementEntry { + requirement: Unnamed( + UnnamedRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/editable-installs/black_editable", + query: None, + fragment: None, + }, + given: Some( + "file:///scripts/editable-installs/black_editable", + ), + }, + extras: [], + marker: None, + }, + ), + hashes: [], + editable: false, + }, + ], + constraints: [], + editables: [], + index_url: None, + extra_index_urls: [], + find_links: [], + no_index: false, +} diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap index c6f1b96ab..80ebe3f58 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap @@ -5,53 +5,57 @@ expression: actual RequirementsTxt { requirements: [ RequirementEntry { - requirement: Requirement { - name: PackageName( - "numpy", - ), - extras: [], - version_or_url: None, - marker: None, - }, + requirement: Pep508( + Requirement { + name: PackageName( + "numpy", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), hashes: [], editable: false, }, RequirementEntry { - requirement: Requirement { - name: PackageName( - "pandas", - ), - extras: [ - ExtraName( - "tabulate", + requirement: Pep508( + Requirement { + name: PackageName( + "pandas", ), - ], - version_or_url: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", + extras: [ + ExtraName( + "tabulate", + ), + ], + version_or_url: Some( + Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + given: Some( + "https://github.com/pandas-dev/pandas", ), - port: None, - path: "/pandas-dev/pandas", - query: None, - fragment: None, }, - given: Some( - "https://github.com/pandas-dev/pandas", - ), - }, + ), ), - ), - marker: None, - }, + marker: None, + }, + ), hashes: [], editable: false, }, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap new file mode 100644 index 000000000..b050166af --- /dev/null +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap @@ -0,0 +1,96 @@ +--- +source: crates/requirements-txt/src/lib.rs +expression: actual +--- +RequirementsTxt { + requirements: [ + RequirementEntry { + requirement: Unnamed( + UnnamedRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/[WORKSPACE_DIR]/scripts/editable-installs/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/editable-installs/black_editable", + ), + }, + extras: [], + marker: None, + }, + ), + hashes: [], + editable: false, + }, + RequirementEntry { + requirement: Unnamed( + UnnamedRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/[WORKSPACE_DIR]/scripts/editable-installs/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/editable-installs/black_editable", + ), + }, + extras: [ + ExtraName( + "dev", + ), + ], + marker: None, + }, + ), + hashes: [], + editable: false, + }, + RequirementEntry { + requirement: Unnamed( + UnnamedRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/[WORKSPACE_DIR]/scripts/editable-installs/black_editable", + query: None, + fragment: None, + }, + given: Some( + "file:///scripts/editable-installs/black_editable", + ), + }, + extras: [], + marker: None, + }, + ), + hashes: [], + editable: false, + }, + ], + constraints: [], + editables: [], + index_url: None, + extra_index_urls: [], + find_links: [], + no_index: false, +} diff --git a/crates/requirements-txt/test-data/requirements-txt/bare-url.txt b/crates/requirements-txt/test-data/requirements-txt/bare-url.txt new file mode 100644 index 000000000..1f2f0bebb --- /dev/null +++ b/crates/requirements-txt/test-data/requirements-txt/bare-url.txt @@ -0,0 +1,3 @@ +./scripts/editable-installs/black_editable +./scripts/editable-installs/black_editable[dev] +file:///scripts/editable-installs/black_editable diff --git a/crates/uv-resolver/src/bare.rs b/crates/uv-resolver/src/bare.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/uv-resolver/src/bare.rs @@ -0,0 +1 @@ + diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index b68550e84..c64af5bd3 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -3,7 +3,7 @@ pub use error::ResolveError; pub use finder::{DistFinder, Reporter as FinderReporter}; pub use manifest::Manifest; pub use options::{Options, OptionsBuilder}; -pub use preferences::Preference; +pub use preferences::{Preference, PreferenceError}; pub use prerelease_mode::PreReleaseMode; pub use python_requirement::PythonRequirement; pub use resolution::{AnnotationStyle, Diagnostic, DisplayResolutionGraph, ResolutionGraph}; @@ -15,6 +15,7 @@ pub use resolver::{ }; pub use version_map::VersionMap; +mod bare; mod candidate_selector; mod constraints; mod dependency_mode; diff --git a/crates/uv-resolver/src/preferences.rs b/crates/uv-resolver/src/preferences.rs index f2010537d..3d966454f 100644 --- a/crates/uv-resolver/src/preferences.rs +++ b/crates/uv-resolver/src/preferences.rs @@ -3,11 +3,21 @@ use std::str::FromStr; use rustc_hash::FxHashMap; use pep440_rs::{Operator, Version}; -use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; +use pep508_rs::{ + MarkerEnvironment, Requirement, RequirementsTxtRequirement, UnnamedRequirement, VersionOrUrl, +}; use pypi_types::{HashError, Hashes}; use requirements_txt::RequirementEntry; use uv_normalize::PackageName; +#[derive(thiserror::Error, Debug)] +pub enum PreferenceError { + #[error("direct URL requirements without package names are not supported: {0}")] + Bare(UnnamedRequirement), + #[error(transparent)] + Hash(#[from] HashError), +} + /// A pinned requirement, as extracted from a `requirements.txt` file. #[derive(Debug)] pub struct Preference { @@ -17,9 +27,14 @@ pub struct Preference { impl Preference { /// Create a [`Preference`] from a [`RequirementEntry`]. - pub fn from_entry(entry: RequirementEntry) -> Result { + pub fn from_entry(entry: RequirementEntry) -> Result { Ok(Self { - requirement: entry.requirement, + requirement: match entry.requirement { + RequirementsTxtRequirement::Pep508(requirement) => requirement, + RequirementsTxtRequirement::Unnamed(requirement) => { + return Err(PreferenceError::Bare(requirement)) + } + }, hashes: entry .hashes .iter() diff --git a/crates/uv/src/requirements.rs b/crates/uv/src/requirements.rs index 1a1ca7bef..a42bbbccf 100644 --- a/crates/uv/src/requirements.rs +++ b/crates/uv/src/requirements.rs @@ -3,20 +3,19 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use console::Term; use indexmap::IndexMap; use rustc_hash::FxHashSet; use tracing::{instrument, Level}; use distribution_types::{FlatIndexLocation, IndexUrl}; -use pep508_rs::Requirement; -use pypi_types::HashError; +use pep508_rs::{Requirement, RequirementsTxtRequirement}; use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt}; use uv_client::Connectivity; use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; -use uv_resolver::Preference; +use uv_resolver::{Preference, PreferenceError}; use uv_warnings::warn_user; use crate::commands::Upgrade; @@ -65,20 +64,6 @@ impl RequirementsSource { } } - // If the user provided a path to a local directory without `-e` (as in - // `uv pip install ../flask`), prompt them to correct it. - if (name.contains('/') || name.contains('\\')) && Path::new(&name).is_dir() { - let term = Term::stderr(); - if term.is_term() { - let prompt = - format!("`{name}` looks like a local directory but was passed as a package name. Did you mean `-e {name}`?"); - let confirmation = confirm::confirm(&prompt, &term, true).unwrap(); - if confirmation { - return Self::RequirementsTxt(name.into()); - } - } - } - Self::Package(name) } } @@ -187,8 +172,13 @@ impl RequirementsSpecification { requirements: requirements_txt .requirements .into_iter() - .map(|entry| entry.requirement) - .collect(), + .map(|entry| match entry.requirement { + RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement), + RequirementsTxtRequirement::Unnamed(requirement) => Err(anyhow!( + "Unnamed URL requirements are not yet supported: {requirement}" + )), + }) + .collect::>>()?, constraints: requirements_txt.constraints, editables: requirements_txt.editables, overrides: vec![], @@ -465,7 +455,7 @@ pub(crate) async fn read_lockfile( .into_iter() .filter(|entry| !entry.editable) .map(Preference::from_entry) - .collect::, HashError>>()?; + .collect::, PreferenceError>>()?; // Apply the upgrade strategy to the requirements. Ok(match upgrade { diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 4c612b4c5..7a5aee51d 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -2474,6 +2474,55 @@ fn respect_http_env_var() -> Result<()> { Ok(()) } +/// A requirement defined as a single unnamed environment variable should be parsed as such. +#[test] +fn respect_unnamed_env_var() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("${URL}")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .env("URL", "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Unnamed URL requirements are not yet supported: https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl + "### + ); + + Ok(()) +} + +/// A requirement defined as a single unnamed environment variable should error if the environment +/// variable is not set. +#[test] +fn error_missing_unnamed_env_var() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("${URL}")?; + + uv_snapshot!(context.compile() + .arg("requirements.in"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Couldn't parse requirement in `requirements.in` at position 0 + Caused by: Expected package name starting with an alphanumeric character, found '$' + ${URL} + ^ + "### + ); + + Ok(()) +} + /// Resolve a dependency from a file path, passing in the entire path as an environment variable. #[test] fn respect_file_env_var() -> Result<()> { @@ -3409,10 +3458,7 @@ fn missing_package_name() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Unsupported requirement in requirements.in at position 0 - Caused by: URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`). - https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + error: Unnamed URL requirements are not yet supported: https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl "### );