├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements.txt ├── rparse.py ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - pypy 6 | install: 7 | - pip install -r requirements.txt 8 | script: 9 | - nosetests 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dmitry Veselov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | include LICENSE 3 | include README.md 4 | include requirements.txt 5 | include rparse.py 6 | include setup.py 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rparse [![Build Status](https://travis-ci.org/dveselov/rparse.svg?branch=master)](https://travis-ci.org/dveselov/rparse) 2 | 3 | Python `requirements.txt` parser. 4 | 5 | # Installation 6 | 7 | ```bash 8 | $ pip install rparse 9 | ``` 10 | 11 | # Usage 12 | 13 | ```python 14 | import rparse 15 | 16 | 17 | requirements = """ 18 | flask == 0.10.1 19 | pip >= 6.0.0, < 6.0.7 20 | """ 21 | 22 | for requirement in rparse.parse(requirements): 23 | print(requirement.name, requirement.specs) 24 | ``` 25 | 26 | Output will be looks like this: 27 | 28 | ```python 29 | ("flask", [("==", "0.10.1")]) 30 | ("pip", [(">=", "6.0.0"), ("<", "6.0.7")]) 31 | ``` 32 | 33 | `rparse` also have simple command line interface that can be used like this: 34 | 35 | ```bash 36 | $ cat requirements.txt 37 | flask==0.10.1 38 | raven[flask]>=1.0 39 | 40 | $ rparse.py requirements.txt 41 | Package: flask 42 | Version Specifier: [('==', '0.10.1')] 43 | Extras: None 44 | Comment: None 45 | ---------------------------------------------------------------- 46 | Package: raven 47 | Version Specifier: [('>=', '1.0')] 48 | Extras: ['flask'] 49 | Comment: None 50 | ---------------------------------------------------------------- 51 | ``` 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ply==3.4 2 | PlyPlus==0.6.1 3 | -------------------------------------------------------------------------------- /rparse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2015, Dmitry Veselov 3 | from __future__ import print_function 4 | from argparse import ArgumentParser 5 | from plyplus import Grammar, STransformer, \ 6 | ParseError, TokenizeError 7 | try: 8 | # Python 2.x and pypy 9 | from itertools import imap as map 10 | from itertools import ifilter as filter 11 | except ImportError: 12 | # Python 3.x already have lazy map 13 | pass 14 | 15 | 16 | __all__ = [ 17 | "parse" 18 | ] 19 | __version__ = "0.2.0" 20 | 21 | 22 | grammar = Grammar(r""" 23 | @start : package ; 24 | 25 | 26 | package : name extras? specs? comment?; 27 | name : string ; 28 | 29 | specs : comparison version (',' comparison version)* ; 30 | comparison : '<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '===' ; 31 | version : string ; 32 | 33 | extras : '\[' (extra (',' extra)*)? '\]' ; 34 | extra : string ; 35 | 36 | comment : '\#.+' ; 37 | 38 | @string : '[-A-Za-z0-9_\.]+' ; 39 | 40 | SPACES: '[ \t\n]+' (%ignore) (%newline); 41 | """) 42 | 43 | 44 | class Requirement(object): 45 | 46 | def __init__(self, name=None, extras=None, specs=None, comment=None): 47 | self.name = name 48 | self.extras = extras 49 | self.specs = specs 50 | self.comment = comment 51 | 52 | def __str__(self): 53 | return "<{0}(name='{1}'>".format(self.__class__.__name__, self.name) 54 | 55 | 56 | class RTransformer(STransformer): 57 | 58 | def package(self, node): 59 | requirement = Requirement() 60 | for key, value in node.tail: 61 | setattr(requirement, key, value) 62 | return requirement 63 | 64 | def name(self, node): 65 | return ("name", node.tail[0]) 66 | 67 | def specs(self, node): 68 | comparisons, versions = node.tail[0::2], node.tail[1::2] 69 | return ("specs", list(zip(comparisons, versions))) 70 | 71 | def comparison(self, node): 72 | return node.tail[0] 73 | 74 | def version(self, node): 75 | return node.tail[0] 76 | 77 | def extras(self, node): 78 | return ("extras", [name for name in node.tail]) 79 | 80 | def extra(self, node): 81 | return node.tail[0] 82 | 83 | def comment(self, node): 84 | return ("comment", node.tail[0]) 85 | 86 | 87 | def _parse(line, g=grammar): 88 | line = line.strip() 89 | if line.startswith("#") or not line: 90 | return None 91 | try: 92 | return g.parse(line) 93 | except (ParseError, TokenizeError): 94 | message = "Invalid requirements line: '{0}'".format(line) 95 | raise ValueError(message) 96 | 97 | 98 | def parse(requirements): 99 | """ 100 | Parses given requirements line-by-line. 101 | """ 102 | transformer = RTransformer() 103 | return map(transformer.transform, filter(None, map(_parse, requirements.splitlines()))) 104 | 105 | 106 | if __name__ == "__main__": 107 | parser = ArgumentParser() 108 | parser.add_argument("path", help="path to requirements.txt file") 109 | args = parser.parse_args() 110 | with open(args.path) as source: 111 | requirements = source.read() 112 | for requirement in parse(requirements): 113 | print("Package: {0}".format(requirement.name)) 114 | print("Version Specifier: {0}".format(requirement.specs)) 115 | print("Extras: {0}".format(requirement.extras)) 116 | print("Comment: {0}".format(requirement.comment)) 117 | print("-" * 64) 118 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from distutils.core import setup 3 | 4 | 5 | def load_requirements(): 6 | with open("requirements.txt") as requirements: 7 | return requirements.read().splitlines() 8 | 9 | 10 | setup(name="rparse", 11 | version="0.2.0", 12 | description="requirements.txt parser", 13 | author="Dmitry Veselov", 14 | author_email="d.a.veselov@yandex.ru", 15 | url="https://github.com/dveselov/rparse", 16 | py_modules=["rparse"], 17 | scripts=["rparse.py"], 18 | install_requires=load_requirements(), 19 | classifiers=[ 20 | "Intended Audience :: Developers", 21 | "Development Status :: 1 - Planning", 22 | "License :: OSI Approved :: MIT License", 23 | 24 | "Programming Language :: Python :: 2.7", 25 | "Programming Language :: Python :: 3.4", 26 | 27 | "Programming Language :: Python :: Implementation :: CPython", 28 | "Programming Language :: Python :: Implementation :: PyPy", 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import rparse 3 | 4 | 5 | class RParseTestCase(unittest.TestCase): 6 | 7 | def test_parse_loosy_requirement(self): 8 | requirements = "flask" 9 | package = next(rparse.parse(requirements)) 10 | self.assertEqual(package.name, "flask") 11 | self.assertEqual(package.specs, None) 12 | 13 | def test_parse_strict_requirement(self): 14 | requirements = "flask==0.10.1" 15 | package = next(rparse.parse(requirements)) 16 | self.assertEqual(package.name, "flask") 17 | self.assertEqual(package.specs, [("==", "0.10.1")]) 18 | 19 | def test_parse_extra_requirements(self): 20 | requirements = "raven[foo, bar]==0.10.1" 21 | package = next(rparse.parse(requirements)) 22 | self.assertEqual(package.name, "raven") 23 | self.assertEqual(package.specs, [("==", "0.10.1")]) 24 | self.assertEqual(package.extras, ["foo", "bar"]) 25 | 26 | def test_parse_multiple_versions(self): 27 | requirements = "flask>=0.10.1, <0.11" 28 | package = next(rparse.parse(requirements)) 29 | self.assertEqual(package.name, "flask") 30 | self.assertEqual(package.specs, [(">=", "0.10.1"), ("<", "0.11")]) 31 | 32 | def test_parse_requirements_with_comments(self): 33 | requirements = "flask==0.10.1 # latest version" 34 | package = next(rparse.parse(requirements)) 35 | self.assertEqual(package.name, "flask") 36 | self.assertEqual(package.comment, "# latest version") 37 | requirements = "flask # latest version" 38 | package = next(rparse.parse(requirements)) 39 | self.assertEqual(package.name, "flask") 40 | self.assertEqual(package.specs, None) 41 | with self.assertRaises(StopIteration): 42 | next(rparse.parse("# comment")) 43 | 44 | def test_parse_invalid_requirements(self): 45 | requirements = """ 46 | flask 0.10.1 47 | redis==1.0 48 | """ 49 | ast = rparse.parse(requirements) 50 | with self.assertRaises(ValueError, message="Invalid requirements line: 'flask 0.10.1'"): 51 | next(ast) 52 | package = next(ast) 53 | self.assertEqual(package.name, "redis") 54 | self.assertEqual(package.specs, [("==", "1.0")]) 55 | with self.assertRaises(ValueError, message="Invalid requirements line: 'flask=0.10.1'"): 56 | next(rparse.parse("flask=0.10.1")) 57 | --------------------------------------------------------------------------------