Parse and store extras on editables (#3629)

## Summary

As a follow-up to #3622, we now parse and store (but don't respect)
markers on editable requirements.
This commit is contained in:
Charlie Marsh 2024-05-16 17:12:52 -04:00 committed by GitHub
parent 7f73f7b3c2
commit ed91b1d562
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 211 additions and 19 deletions

View file

@ -137,7 +137,7 @@ create_exception!(
/// A PEP 508 dependency specifier.
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
pub struct Requirement<T: Pep508Url = VerbatimUrl> {
/// The distribution name such as `numpy` in
/// The distribution name such as `requests` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`.
pub name: PackageName,
/// The list of extras such as `security`, `tests` in
@ -145,7 +145,7 @@ pub struct Requirement<T: Pep508Url = VerbatimUrl> {
pub extras: Vec<ExtraName>,
/// The version specifier such as `>= 2.8.1`, `== 2.8.*` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`.
/// or a url
/// or a URL.
pub version_or_url: Option<VersionOrUrl<T>>,
/// The markers such as `python_version > "3.8"` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`.
@ -241,7 +241,7 @@ impl Deref for PyRequirement {
#[cfg(feature = "pyo3")]
#[pymethods]
impl PyRequirement {
/// The distribution name such as `numpy` in
/// The distribution name such as `requests` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`
#[getter]
pub fn name(&self) -> String {

View file

@ -48,7 +48,7 @@ use distribution_types::{
ParsedUrlError, Requirement, UnresolvedRequirement, UnresolvedRequirementSpecification,
};
use pep508_rs::{
expand_env_vars, split_scheme, strip_host, Extras, Pep508Error, Pep508ErrorSource,
expand_env_vars, split_scheme, strip_host, Extras, MarkerTree, Pep508Error, Pep508ErrorSource,
RequirementOrigin, Scheme, VerbatimUrl,
};
#[cfg(feature = "http")]
@ -168,6 +168,8 @@ pub struct EditableRequirement {
pub url: VerbatimUrl,
/// The extras that should be included when resolving the editable requirements.
pub extras: Vec<ExtraName>,
/// The markers such as `python_version > "3.8"` in `-e ../editable ; python_version > "3.8"`.
pub marker: Option<MarkerTree>,
/// The local path to the editable.
pub path: PathBuf,
/// The source file containing the requirement.
@ -199,10 +201,37 @@ impl EditableRequirement {
origin: Option<&Path>,
working_dir: impl AsRef<Path>,
) -> Result<Self, RequirementsTxtParserError> {
// Identify (but discard) any markers, to match pip.
let given = Self::split_markers(given)
.map(|(requirement, _)| requirement)
.unwrap_or(given);
// Identify the markers.
let (given, marker) = if let Some((requirement, marker)) = Self::split_markers(given) {
let marker = MarkerTree::from_str(marker).map_err(|err| {
// Map from error on the markers to error on the whole requirement.
let err = Pep508Error {
message: err.message,
start: requirement.len() + err.start,
len: err.len,
input: given.to_string(),
};
match err.message {
Pep508ErrorSource::String(_) | Pep508ErrorSource::UrlError(_) => {
RequirementsTxtParserError::Pep508 {
start: err.start,
end: err.start + err.len,
source: err,
}
}
Pep508ErrorSource::UnsupportedRequirement(_) => {
RequirementsTxtParserError::UnsupportedRequirement {
start: err.start,
end: err.start + err.len,
source: err,
}
}
}
})?;
(requirement, Some(marker))
} else {
(given, None)
};
// Identify the extras.
let (requirement, extras) = if let Some((requirement, extras)) = Self::split_extras(given) {
@ -279,6 +308,7 @@ impl EditableRequirement {
Ok(Self {
url,
extras,
marker,
path,
origin: origin.map(Path::to_path_buf).map(RequirementOrigin::File),
})
@ -322,7 +352,27 @@ impl EditableRequirement {
} else if c == ']' {
depth -= 1;
} else if depth == 0 && c.is_whitespace() {
return Some(given.split_at(index));
// We found the end of the requirement; now, find the start of the markers,
// delimited by a semicolon.
let (requirement, markers) = given.split_at(index);
// Skip the whitespace.
for (index, c) in markers.char_indices() {
if backslash {
backslash = false;
} else if c == '\\' {
backslash = true;
} else if c.is_whitespace() {
continue;
} else if c == ';' {
// The marker starts just after the semicolon.
let markers = &markers[index + 1..];
return Some((requirement, markers));
} else {
// We saw some other character, so this isn't a marker.
return None;
}
}
}
}
None
@ -1995,6 +2045,7 @@ mod test {
),
},
extras: [],
marker: None,
path: "/foo/bar",
origin: Some(
File(

View file

@ -31,6 +31,7 @@ RequirementsTxt {
"dev",
),
],
marker: None,
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -63,6 +64,7 @@ RequirementsTxt {
"dev",
),
],
marker: None,
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -95,6 +97,28 @@ RequirementsTxt {
"dev",
),
],
marker: Some(
And(
[
Expression(
Version {
key: PythonVersion,
specifier: VersionSpecifier {
operator: GreaterThanEqual,
version: "3.9",
},
},
),
Expression(
String {
key: OsName,
operator: Equal,
value: "posix",
},
),
],
),
),
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -127,6 +151,28 @@ RequirementsTxt {
"dev",
),
],
marker: Some(
And(
[
Expression(
Version {
key: PythonVersion,
specifier: VersionSpecifier {
operator: GreaterThanEqual,
version: "3.9",
},
},
),
Expression(
String {
key: OsName,
operator: Equal,
value: "posix",
},
),
],
),
),
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -152,6 +198,28 @@ RequirementsTxt {
),
},
extras: [],
marker: Some(
And(
[
Expression(
Version {
key: PythonVersion,
specifier: VersionSpecifier {
operator: GreaterThanEqual,
version: "3.9",
},
},
),
Expression(
String {
key: OsName,
operator: Equal,
value: "posix",
},
),
],
),
),
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -168,16 +236,17 @@ RequirementsTxt {
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable;",
path: "<REQUIREMENTS_DIR>/editable;%20python_version%20%3E=%20%223.9%22%20and%20os_name%20==%20%22posix%22",
query: None,
fragment: None,
},
given: Some(
"./editable;",
"./editable; python_version >= \"3.9\" and os_name == \"posix\"",
),
},
extras: [],
path: "<REQUIREMENTS_DIR>/editable;",
marker: None,
path: "<REQUIREMENTS_DIR>/editable; python_version >= \"3.9\" and os_name == \"posix\"",
origin: Some(
File(
"<REQUIREMENTS_DIR>/editable.txt",

View file

@ -31,6 +31,7 @@ RequirementsTxt {
"dev",
),
],
marker: None,
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -63,6 +64,7 @@ RequirementsTxt {
"dev",
),
],
marker: None,
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -95,6 +97,28 @@ RequirementsTxt {
"dev",
),
],
marker: Some(
And(
[
Expression(
Version {
key: PythonVersion,
specifier: VersionSpecifier {
operator: GreaterThanEqual,
version: "3.9",
},
},
),
Expression(
String {
key: OsName,
operator: Equal,
value: "posix",
},
),
],
),
),
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -127,6 +151,28 @@ RequirementsTxt {
"dev",
),
],
marker: Some(
And(
[
Expression(
Version {
key: PythonVersion,
specifier: VersionSpecifier {
operator: GreaterThanEqual,
version: "3.9",
},
},
),
Expression(
String {
key: OsName,
operator: Equal,
value: "posix",
},
),
],
),
),
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -152,6 +198,28 @@ RequirementsTxt {
),
},
extras: [],
marker: Some(
And(
[
Expression(
Version {
key: PythonVersion,
specifier: VersionSpecifier {
operator: GreaterThanEqual,
version: "3.9",
},
},
),
Expression(
String {
key: OsName,
operator: Equal,
value: "posix",
},
),
],
),
),
path: "<REQUIREMENTS_DIR>/editable",
origin: Some(
File(
@ -168,16 +236,17 @@ RequirementsTxt {
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable;",
path: "/<REQUIREMENTS_DIR>/editable;%20python_version%20%3E=%20%223.9%22%20and%20os_name%20==%20%22posix%22",
query: None,
fragment: None,
},
given: Some(
"./editable;",
"./editable; python_version >= \"3.9\" and os_name == \"posix\"",
),
},
extras: [],
path: "<REQUIREMENTS_DIR>/editable;",
marker: None,
path: "<REQUIREMENTS_DIR>/editable; python_version >= \"3.9\" and os_name == \"posix\"",
origin: Some(
File(
"<REQUIREMENTS_DIR>/editable.txt",

View file

@ -5,13 +5,13 @@
-e ./editable[d, dev]
# OK
-e ./editable[d,dev] ; python_version >= "3.9" and python_ver
-e ./editable[d,dev] ; python_version >= "3.9" and os_name == "posix"
# OK (whitespace between extras; disallowed by pip)
-e ./editable[d, dev] ; python_version >= "3.9" and python_ver
-e ./editable[d, dev] ; python_version >= "3.9" and os_name == "posix"
# OK
-e ./editable ; python_version >= "3.9" and python_ver
-e ./editable ; python_version >= "3.9" and os_name == "posix"
# Disallowed (missing whitespace before colon)
-e ./editable; python_version >= "3.9" and python_ver
-e ./editable; python_version >= "3.9" and os_name == "posix"

View file

@ -191,6 +191,7 @@ impl RequirementsSpecification {
Either::Left(EditableRequirement {
url,
path,
marker: requirement.marker,
extras: requirement.extras,
origin: requirement.origin,
})

View file

@ -427,6 +427,7 @@ pub(crate) async fn pip_compile(
url,
extras,
path,
marker: _,
origin: _,
} = editable;
LocalEditable { url, path, extras }

View file

@ -108,6 +108,7 @@ impl ResolvedEditables {
url,
path,
extras,
marker: _,
origin: _,
} = editable;
LocalEditable {