Skip to content

Commit

Permalink
requirements
Browse files Browse the repository at this point in the history
  • Loading branch information
batisteo committed Apr 4, 2020
0 parents commit 15cacb6
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "requirements"
version = "0.1.0"
authors = ["Baptiste Darthenay <[email protected]>"]
description = "Python PIP requirements.txt parser"
keywords = ["python", "pip", "requirements"]
categories = ["parser-implementations"]
readme = "README.md"
edition = "2018"
license = "MIT"
repository = "https://github.com/batisteo/requirements"

[lib]
name = "requirements"
path = "./src/lib.rs"

[dependencies]
regex = "^1"
pest = "^2"
pest_derive = "^2"
globwalk = "^0.7"
walkdir = "^2"
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# requirements

Fast parser of requirement files (txt or in)

## Usage
__Soon…__

License: MIT
38 changes: 38 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use globwalk::GlobError;
use requirements::{self, Requirement};
use std::{env, fs, path::Path};
use walkdir::DirEntry;

fn find_req_files() -> Result<Vec<DirEntry>, GlobError> {
let globwalker = globwalk::GlobWalkerBuilder::from_patterns(".", &["*req*.{txt,in}"])
.max_depth(4)
.follow_links(true)
.build()?
.into_iter()
.filter_map(Result::ok)
.collect();
Ok(globwalker)
}

fn parse_requirements(path: &Path) -> () {
let content = fs::read_to_string(&path).expect("Cannot read file");
let requirement_file: Vec<Requirement> = requirements::parse(&content).unwrap();
for requirement in requirement_file.into_iter() {
println!("{:?}", requirement.line);
}
}

fn show_parsing(args: &Vec<String>) -> () {
if args.len() > 1 {
parse_requirements(Path::new(&args[1]))
} else {
for path in find_req_files().unwrap() {
parse_requirements(&path.path())
}
}
}

fn main() {
let args: Vec<String> = env::args().collect();
show_parsing(&args);
}
48 changes: 48 additions & 0 deletions src/enums/comparison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::Rule;
use std::fmt;

#[derive(Debug)]
pub enum Comparison {
LessThan,
LessThanOrEqual,
NotEqual,
Equal,
GreaterThanOrEqual,
GreaterThan,
CompatibleRelease,
ArbitraryEqual,
}

impl Comparison {
pub fn from_rule(rule: Rule) -> Comparison {
use Comparison::*;
match rule {
Rule::less_than => LessThan,
Rule::less_than_or_equal => LessThanOrEqual,
Rule::not_equal => NotEqual,
Rule::equal => Equal,
Rule::greater_than_or_equal => GreaterThanOrEqual,
Rule::greater_than => GreaterThan,
Rule::compatible_release => CompatibleRelease,
Rule::arbitrary_equal => ArbitraryEqual,
_ => ArbitraryEqual,
}
}
}

impl fmt::Display for Comparison {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Comparison::*;
let sign = match self {
LessThan => "<",
LessThanOrEqual => "<=",
NotEqual => "!=",
Equal => "==",
GreaterThanOrEqual => ">=",
GreaterThan => ">",
CompatibleRelease => "~=",
ArbitraryEqual => "===",
};
write!(f, "{}", sign)
}
}
5 changes: 5 additions & 0 deletions src/enums/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod comparison;
pub mod vcs;

pub use comparison::Comparison;
pub use vcs::VersionControlSystem;
8 changes: 8 additions & 0 deletions src/enums/vcs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[derive(Debug)]
pub enum VersionControlSystem {
Git,
Mercurial,
Subversion,
Bazaar,
Unknown,
}
130 changes: 130 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Fast parser of requirement files (txt or in)
//!
//! # Usage
//! __Soon…__

#[macro_use]
extern crate pest_derive;

mod enums;

use enums::{Comparison, VersionControlSystem};
use pest::error::Error;
use pest::iterators::Pair;
use pest::Parser;
use std::path::Path;

impl VersionControlSystem {
pub fn _from_rule(rule: Rule) -> VersionControlSystem {
use VersionControlSystem::*;
match rule {
Rule::git => Git,
Rule::hg => Mercurial,
Rule::svn => Subversion,
Rule::bzr => Bazaar,
_ => Unknown,
}
}
}

#[derive(Parser)]
#[grammar = "requirements.pest"]
pub struct RequirementParser;

#[derive(Debug)]
pub struct Requirement<'a> {
pub line: &'a str,
pub editable: bool,
pub name: &'a str,
pub comment: Option<&'a str>,
pub specifier: bool,
pub vcs: Option<VersionControlSystem>,
pub uri: Option<&'a Path>,
pub subdirectory: Option<&'a str>,
pub path: Option<&'a Path>,
pub hash_name: Option<&'a str>,
pub hash: Option<&'a str>,
pub revision: Option<&'a str>,
pub extras: Vec<&'a str>,
pub specs: Vec<(Comparison, &'a str)>,
pub extra_index_url: &'a str,
}

impl Requirement<'_> {
fn new<'a>() -> Requirement<'a> {
Requirement {
line: "",
name: "",
specs: vec![],
extras: vec![],
comment: None,
specifier: true,
editable: false,
uri: None,
subdirectory: None,
path: None,
hash_name: None,
hash: None,
revision: None,
vcs: None,
extra_index_url: "",
}
}
}

fn parse_version(pair: Pair<Rule>) -> (Comparison, &str) {
let mut version = pair.into_inner();
let comparison = Comparison::from_rule(version.next().unwrap().as_rule());
let number = version.next().unwrap().as_str();
(comparison, number)
}

fn parse_package(parsed: Pair<'_, Rule>) -> Requirement<'_> {
let mut package = Requirement::new();
for line in parsed.into_inner() {
package.line = line.as_str();
for pair in line.into_inner() {
match &pair.as_rule() {
Rule::name => package.name = pair.as_str(),
Rule::version => package.specs.push(parse_version(pair)),
Rule::extras => {
package.extras = pair.into_inner().map(|extra| extra.as_str()).collect()
}
Rule::comment => {
package.comment = {
let comment = pair.into_inner().next().unwrap().as_str();
if comment.is_empty() {
None
} else {
Some(comment)
}
}
}
Rule::extra_index_url => package.extra_index_url = pair.as_str(),
_ => (),
}
}
}
package
}

pub fn parse(unparsed_file: &str) -> Result<Vec<Requirement>, Error<Rule>> {
let requirement_file = RequirementParser::parse(Rule::requirement_file, &unparsed_file)?
.next()
.unwrap();

// copy_of_requirement_file = requirement_file.copy();
let requirements: Vec<Requirement> = requirement_file
.into_inner()
.filter(|pair| pair.as_rule() == Rule::line)
.map(parse_package)
.collect();

// let metas: Vec<Requirement> = copy_of_requirement_file
// .into_inner()
// .filter(|pair| pair.as_rule() == Rule::meta)
// .map(parse_package)
// .collect();
// println!("{:?}", metas);
Ok(requirements)
}
43 changes: 43 additions & 0 deletions src/requirements.pest
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
WHITESPACE = _{ " " | "\t" }

requirement_file = { SOI ~ ((line)? ~ NEWLINE)* }

// meta = { meta | requirement }
line = { extra_index_url | comment | package | path }

package = { name ~ extras? ~ (version | ",")* ~ comment? }
name = @{ LETTER ~ chars }
extras = { "[" ~ (extra | ",")+ ~ "]" }
extra = @{ chars }
version = { comparison ~ number }

path = { editable? ~ chars }
editable = { ("-e" | "--editable") }

comment = { "#" ~ some_text }

extra_index_url = { "--extra-index-url" ~ url }


comparison = _{ less_than | less_than_or_equal | not_equal | equal | greater_than_or_equal | greater_than | compatible_release | arbitrary_equal }
less_than = { "<" }
less_than_or_equal = { "<=" }
not_equal = { "!=" }
equal = { "==" }
greater_than_or_equal = { ">=" }
greater_than = { ">" }
compatible_release = { "~=" }
arbitrary_equal = { "===" }

vcs = _{ git | hg | svn | bzr }
git = { "git" }
hg = { "hg" }
svn = { "svn" }
bzr = { "bzr" }

chars = _{ (ASCII_ALPHANUMERIC | "_" | "-")+ }
version_char = _{ (ASCII_ALPHANUMERIC | ".") }
number = @{ version_char+ }
some_char = _{ (LETTER | NUMBER | PUNCTUATION | SYMBOL) }
some_text = { some_char* }
url = @{ "http" ~ "s"? ~ "://" ~ some_char+ }
1 change: 1 addition & 0 deletions src/versions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions tests/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
aiohttp
passlib[argon2,chacha]
9 changes: 9 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Example requirement file
--extra-index-url https://pypi.example.com/pypi

aiohttp
black==19.10b0
chardet # upgrade at will
Django>=2,<3
-e editable_package
passlib[argon2,chacha]

0 comments on commit 15cacb6

Please sign in to comment.