use super::WrappedPackage;
const ECO: &str = "pypi";
const VERB: &str = "install";
pub fn parse(argv: &[&str]) -> Vec<WrappedPackage> {
let Some(verb_pos) = argv.iter().position(|a| *a == VERB) else {
return Vec::new();
};
let mut packages = Vec::new();
let mut iter = argv.iter().skip(verb_pos + 1);
while let Some(arg) = iter.next() {
if matches!(*arg, "-e" | "--editable") {
let _ = iter.next();
continue;
}
if matches!(*arg, "-r" | "--requirement") {
let _ = iter.next();
continue;
}
if matches!(*arg, "-c" | "--constraint") {
let _ = iter.next();
continue;
}
if VALUE_FLAGS.contains(arg) {
let _ = iter.next();
continue;
}
if let Some(stripped) = arg.strip_prefix("--") {
let _ = stripped;
continue;
}
if arg.starts_with('-') {
continue;
}
if arg.is_empty() {
continue;
}
if arg.starts_with('.') || arg.starts_with('/') || arg.contains("://") {
continue;
}
let (name, version) = split_pkg_spec(arg);
packages.push(WrappedPackage::new(ECO, name, version));
}
packages
}
const VALUE_FLAGS: &[&str] = &[
"-i",
"--index-url",
"--extra-index-url",
"--find-links",
"--target",
"--platform",
"--python-version",
"--implementation",
"--abi",
"--prefix",
"--user",
];
pub fn split_pkg_spec(spec: &str) -> (String, String) {
if let Some((name, ver)) = spec.split_once("==") {
let name = name.trim();
let ver = ver.trim();
let name = match name.split_once('[') {
Some((bare, _)) => bare,
None => name,
};
return (name.to_string(), ver.to_string());
}
let mut name = spec.to_string();
for op in ["~=", ">=", "<=", "!=", ">", "<"] {
if let Some((n, _)) = name.split_once(op) {
name = n.trim().to_string();
break;
}
}
if let Some((bare, _)) = name.split_once('[') {
name = bare.to_string();
}
(name, "latest".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn argv<'a>(args: &[&'a str]) -> Vec<&'a str> {
let mut v: Vec<&'a str> = vec!["pip"];
v.extend_from_slice(args);
v
}
#[test]
fn parses_exact_pin() {
let a = argv(&["install", "requests==2.32.5"]);
let pkgs = parse(&a);
assert_eq!(pkgs[0], WrappedPackage::new("pypi", "requests", "2.32.5"));
}
#[test]
fn parses_unpinned() {
let a = argv(&["install", "django"]);
let pkgs = parse(&a);
assert_eq!(pkgs[0], WrappedPackage::new("pypi", "django", "latest"));
}
#[test]
fn parses_range_returns_latest() {
let a = argv(&["install", "django>=4.2"]);
let pkgs = parse(&a);
assert_eq!(pkgs[0], WrappedPackage::new("pypi", "django", "latest"));
}
#[test]
fn skips_path_install() {
let a = argv(&["install", "./my-local-pkg"]);
assert!(parse(&a).is_empty());
}
#[test]
fn skips_requirements_file() {
let a = argv(&["install", "-r", "requirements.txt"]);
assert!(parse(&a).is_empty());
}
#[test]
fn strips_extras() {
let a = argv(&["install", "requests[security]==2.32.5"]);
let pkgs = parse(&a);
assert_eq!(pkgs[0].name, "requests");
assert_eq!(pkgs[0].version, "2.32.5");
}
#[test]
fn skips_value_flag() {
let a = argv(&["install", "--index-url", "https://pypi.org/simple", "requests==2.32.5"]);
let pkgs = parse(&a);
assert_eq!(pkgs.len(), 1);
assert_eq!(pkgs[0].name, "requests");
}
#[test]
fn multiple_packages() {
let a = argv(&["install", "django>=4.2", "celery==5.3.0"]);
let pkgs = parse(&a);
assert_eq!(pkgs.len(), 2);
assert_eq!(pkgs[0].name, "django");
assert_eq!(pkgs[1], WrappedPackage::new("pypi", "celery", "5.3.0"));
}
#[test]
fn no_install_verb_returns_empty() {
let a = argv(&["list"]);
assert!(parse(&a).is_empty());
}
}