diff --git a/HISTORY.rst b/HISTORY.rst index 764e7f5..054b4b2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,22 @@ History ======= +0.2.1 (2017-07-19) +------------------ + +* Internal refactoring + +0.2.0 (2017-07-19) +------------------ + +* Removed setuptools dependency + + +0.1.1 (2017-07-14) +------------------ + +* Fixed a bug that was causing the parser to throw errors on invalid requirements. + 0.1.0 (2017-07-11) ------------------ diff --git a/README.rst b/README.rst index 90ea92e..c925a2b 100644 --- a/README.rst +++ b/README.rst @@ -46,12 +46,15 @@ Supported Files +------------------+------------+-----------+ | setup.py | no (# 2_) | no (# 2_) | +------------------+------------+-----------+ -| zc.bildout | no (# 3_) | no (# 3_) | +| zc.buildout | no (# 3_) | no (# 3_) | ++------------------+------------+-----------+ +| setup.cfg | no (# 4_) | no (# 4_) | +------------------+------------+-----------+ .. _1: https://github.com/pyupio/dparse/issues/1 .. _2: https://github.com/pyupio/dparse/issues/2 .. _3: https://github.com/pyupio/dparse/issues/3 +.. _4: https://github.com/pyupio/dparse/issues/8 ************ Installation diff --git a/dparse/__init__.py b/dparse/__init__.py index 4e0636e..6073b0e 100644 --- a/dparse/__init__.py +++ b/dparse/__init__.py @@ -4,6 +4,6 @@ __author__ = """Jannis Gebauer""" __email__ = 'ja.geb@me.com' -__version__ = '0.1.0' +__version__ = '0.2.1' from .parser import parse diff --git a/dparse/parser.py b/dparse/parser.py index 508f3c9..c8619eb 100644 --- a/dparse/parser.py +++ b/dparse/parser.py @@ -18,10 +18,62 @@ from .regex import URL_REGEX, HASH_REGEX from .dependencies import DependencyFile, Dependency -from pkg_resources import parse_requirements +from packaging.requirements import Requirement as PackagingRequirement, InvalidRequirement +import six from . import filetypes +# this is a backport from setuptools 26.1 +def setuptools_parse_requirements_backport(strs): # pragma: no cover + # Copyright (C) 2016 Jason R Coombs + # + # Permission is hereby granted, free of charge, to any person obtaining a copy of + # this software and associated documentation files (the "Software"), to deal in + # the Software without restriction, including without limitation the rights to + # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + # of the Software, and to permit persons to whom the Software is furnished to do + # so, subject to the following conditions: + # + # The above copyright notice and this permission notice shall be included in all + # copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + # SOFTWARE. + """Yield ``Requirement`` objects for each specification in `strs` + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + # create a steppable iterator, so we can handle \-continuations + def yield_lines(strs): + """Yield non-empty/non-comment lines of a string or sequence""" + if isinstance(strs, six.string_types): + for s in strs.splitlines(): + s = s.strip() + # skip blank lines/comments + if s and not s.startswith('#'): + yield s + else: + for ss in strs: + for s in yield_lines(ss): + yield s + lines = iter(yield_lines(strs)) + + for line in lines: + # Drop comments -- a hash without a space may be in a URL. + if ' #' in line: + line = line[:line.find(' #')] + # If there is a line continuation, drop it, and append the next line. + if line.endswith('\\'): + line = line[:-2].strip() + line += next(lines) + yield PackagingRequirement(line) + + class RequirementsTXTLineParser(object): """ @@ -34,14 +86,17 @@ def parse(cls, line): :param line: :return: """ - # setuptools requires a space before the comment. If this isn't the case, add it. - if "\t#" in line: - parsed, = parse_requirements(line.replace("\t#", "\t #")) - else: - parsed, = parse_requirements(line) + try: + # setuptools requires a space before the comment. If this isn't the case, add it. + if "\t#" in line: + parsed, = setuptools_parse_requirements_backport(line.replace("\t#", "\t #")) + else: + parsed, = setuptools_parse_requirements_backport(line) + except InvalidRequirement: + return None dep = Dependency( - name=parsed.project_name, - specs=parsed.specs, + name=parsed.name, + specs=parsed.specifier, line=line, extras=parsed.extras, dependency_type=filetypes.requirements_txt @@ -200,11 +255,12 @@ def parse(self): parseable_line, hashes = Parser.parse_hashes(parseable_line) req = RequirementsTXTLineParser.parse(parseable_line) - req.hashes = hashes - req.index_server = index_server - # replace the requirements line with the 'real' line - req.line = line - self.obj.dependencies.append(req) + if req: + req.hashes = hashes + req.index_server = index_server + # replace the requirements line with the 'real' line + req.line = line + self.obj.dependencies.append(req) except ValueError: continue @@ -229,8 +285,9 @@ def parse(self): continue if line: req = RequirementsTXTLineParser.parse(line) - req.dependency_type = self.obj.file_type - self.obj.dependencies.append(req) + if req: + req.dependency_type = self.obj.file_type + self.obj.dependencies.append(req) except NoOptionError: pass @@ -254,8 +311,9 @@ def parse(self): if self.is_marked_line(line): continue req = RequirementsTXTLineParser.parse(line) - req.dependency_type = self.obj.file_type - self.obj.dependencies.append(req) + if req: + req.dependency_type = self.obj.file_type + self.obj.dependencies.append(req) except yaml.YAMLError: pass diff --git a/setup.py b/setup.py index 3ceef80..de631d9 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,8 @@ history = history_file.read() requirements = [ - "setuptools<=26.1.1", + "packaging", + "six", "pyyaml", ] @@ -26,7 +27,7 @@ setup( name='dparse', - version='0.1.0', + version='0.2.1', description="A parser for Python dependency files", long_description=readme + '\n\n' + history, author="Jannis Gebauer", diff --git a/tests/test_parse.py b/tests/test_parse.py index 69c0cdc..cff0722 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -7,6 +7,38 @@ from dparse import filetypes +def test_requirements_with_invalid_requirement(): + + content = "in=vali===d{}{}{" + dep_file = parse(content, file_type=filetypes.requirements_txt) + assert len(dep_file.dependencies) == 0 + + +def test_tox_ini_with_invalid_requirement(): + + content = "[testenv]" \ + "passenv = CI TRAVIS TRAVIS_*" \ + "setenv =" \ + "PYTHONPATH = {toxinidir}" \ + "deps =" \ + "-r{toxinidir}/requirements_dev.txt" \ + "pytest-cov" \ + "codecov" + dep_file = parse(content, file_type=filetypes.tox_ini) + assert len(dep_file.dependencies) == 0 + + +def test_conda_file_with_invalid_requirement(): + + content = "name: my_env\n" \ + "dependencies:\n" \ + " - gevent=1.2.1\n" \ + " - pip:\n" \ + " - in=vali===d{}{}{" + dep_file = parse(content, file_type=filetypes.conda_yml) + assert len(dep_file.dependencies) == 0 + + def test_conda_file_invalid_yml(): content = "wawth:dda : awd:\ndlll"