Skip to content

Commit d66d62c

Browse files
authored
Merge pull request #2 from pyupio/master
Latest master
2 parents 47b881f + ec425c9 commit d66d62c

6 files changed

Lines changed: 131 additions & 21 deletions

File tree

HISTORY.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
History
33
=======
44

5+
0.2.1 (2017-07-19)
6+
------------------
7+
8+
* Internal refactoring
9+
10+
0.2.0 (2017-07-19)
11+
------------------
12+
13+
* Removed setuptools dependency
14+
15+
16+
0.1.1 (2017-07-14)
17+
------------------
18+
19+
* Fixed a bug that was causing the parser to throw errors on invalid requirements.
20+
521
0.1.0 (2017-07-11)
622
------------------
723

README.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ Supported Files
4646
+------------------+------------+-----------+
4747
| setup.py | no (# 2_) | no (# 2_) |
4848
+------------------+------------+-----------+
49-
| zc.bildout | no (# 3_) | no (# 3_) |
49+
| zc.buildout | no (# 3_) | no (# 3_) |
50+
+------------------+------------+-----------+
51+
| setup.cfg | no (# 4_) | no (# 4_) |
5052
+------------------+------------+-----------+
5153

5254
.. _1: https://github.com/pyupio/dparse/issues/1
5355
.. _2: https://github.com/pyupio/dparse/issues/2
5456
.. _3: https://github.com/pyupio/dparse/issues/3
57+
.. _4: https://github.com/pyupio/dparse/issues/8
5558

5659
************
5760
Installation

dparse/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
__author__ = """Jannis Gebauer"""
66
__email__ = 'ja.geb@me.com'
7-
__version__ = '0.1.0'
7+
__version__ = '0.2.1'
88

99
from .parser import parse

dparse/parser.py

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,62 @@
1818
from .regex import URL_REGEX, HASH_REGEX
1919

2020
from .dependencies import DependencyFile, Dependency
21-
from pkg_resources import parse_requirements
21+
from packaging.requirements import Requirement as PackagingRequirement, InvalidRequirement
22+
import six
2223
from . import filetypes
2324

2425

26+
# this is a backport from setuptools 26.1
27+
def setuptools_parse_requirements_backport(strs): # pragma: no cover
28+
# Copyright (C) 2016 Jason R Coombs <jaraco@jaraco.com>
29+
#
30+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
31+
# this software and associated documentation files (the "Software"), to deal in
32+
# the Software without restriction, including without limitation the rights to
33+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
34+
# of the Software, and to permit persons to whom the Software is furnished to do
35+
# so, subject to the following conditions:
36+
#
37+
# The above copyright notice and this permission notice shall be included in all
38+
# copies or substantial portions of the Software.
39+
#
40+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46+
# SOFTWARE.
47+
"""Yield ``Requirement`` objects for each specification in `strs`
48+
49+
`strs` must be a string, or a (possibly-nested) iterable thereof.
50+
"""
51+
# create a steppable iterator, so we can handle \-continuations
52+
def yield_lines(strs):
53+
"""Yield non-empty/non-comment lines of a string or sequence"""
54+
if isinstance(strs, six.string_types):
55+
for s in strs.splitlines():
56+
s = s.strip()
57+
# skip blank lines/comments
58+
if s and not s.startswith('#'):
59+
yield s
60+
else:
61+
for ss in strs:
62+
for s in yield_lines(ss):
63+
yield s
64+
lines = iter(yield_lines(strs))
65+
66+
for line in lines:
67+
# Drop comments -- a hash without a space may be in a URL.
68+
if ' #' in line:
69+
line = line[:line.find(' #')]
70+
# If there is a line continuation, drop it, and append the next line.
71+
if line.endswith('\\'):
72+
line = line[:-2].strip()
73+
line += next(lines)
74+
yield PackagingRequirement(line)
75+
76+
2577
class RequirementsTXTLineParser(object):
2678
"""
2779
@@ -34,14 +86,17 @@ def parse(cls, line):
3486
:param line:
3587
:return:
3688
"""
37-
# setuptools requires a space before the comment. If this isn't the case, add it.
38-
if "\t#" in line:
39-
parsed, = parse_requirements(line.replace("\t#", "\t #"))
40-
else:
41-
parsed, = parse_requirements(line)
89+
try:
90+
# setuptools requires a space before the comment. If this isn't the case, add it.
91+
if "\t#" in line:
92+
parsed, = setuptools_parse_requirements_backport(line.replace("\t#", "\t #"))
93+
else:
94+
parsed, = setuptools_parse_requirements_backport(line)
95+
except InvalidRequirement:
96+
return None
4297
dep = Dependency(
43-
name=parsed.project_name,
44-
specs=parsed.specs,
98+
name=parsed.name,
99+
specs=parsed.specifier,
45100
line=line,
46101
extras=parsed.extras,
47102
dependency_type=filetypes.requirements_txt
@@ -200,11 +255,12 @@ def parse(self):
200255
parseable_line, hashes = Parser.parse_hashes(parseable_line)
201256

202257
req = RequirementsTXTLineParser.parse(parseable_line)
203-
req.hashes = hashes
204-
req.index_server = index_server
205-
# replace the requirements line with the 'real' line
206-
req.line = line
207-
self.obj.dependencies.append(req)
258+
if req:
259+
req.hashes = hashes
260+
req.index_server = index_server
261+
# replace the requirements line with the 'real' line
262+
req.line = line
263+
self.obj.dependencies.append(req)
208264
except ValueError:
209265
continue
210266

@@ -229,8 +285,9 @@ def parse(self):
229285
continue
230286
if line:
231287
req = RequirementsTXTLineParser.parse(line)
232-
req.dependency_type = self.obj.file_type
233-
self.obj.dependencies.append(req)
288+
if req:
289+
req.dependency_type = self.obj.file_type
290+
self.obj.dependencies.append(req)
234291
except NoOptionError:
235292
pass
236293

@@ -254,8 +311,9 @@ def parse(self):
254311
if self.is_marked_line(line):
255312
continue
256313
req = RequirementsTXTLineParser.parse(line)
257-
req.dependency_type = self.obj.file_type
258-
self.obj.dependencies.append(req)
314+
if req:
315+
req.dependency_type = self.obj.file_type
316+
self.obj.dependencies.append(req)
259317
except yaml.YAMLError:
260318
pass
261319

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
history = history_file.read()
1313

1414
requirements = [
15-
"setuptools<=26.1.1",
15+
"packaging",
16+
"six",
1617
"pyyaml",
1718
]
1819

@@ -26,7 +27,7 @@
2627

2728
setup(
2829
name='dparse',
29-
version='0.1.0',
30+
version='0.2.1',
3031
description="A parser for Python dependency files",
3132
long_description=readme + '\n\n' + history,
3233
author="Jannis Gebauer",

tests/test_parse.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,38 @@
77
from dparse import filetypes
88

99

10+
def test_requirements_with_invalid_requirement():
11+
12+
content = "in=vali===d{}{}{"
13+
dep_file = parse(content, file_type=filetypes.requirements_txt)
14+
assert len(dep_file.dependencies) == 0
15+
16+
17+
def test_tox_ini_with_invalid_requirement():
18+
19+
content = "[testenv]" \
20+
"passenv = CI TRAVIS TRAVIS_*" \
21+
"setenv =" \
22+
"PYTHONPATH = {toxinidir}" \
23+
"deps =" \
24+
"-r{toxinidir}/requirements_dev.txt" \
25+
"pytest-cov" \
26+
"codecov"
27+
dep_file = parse(content, file_type=filetypes.tox_ini)
28+
assert len(dep_file.dependencies) == 0
29+
30+
31+
def test_conda_file_with_invalid_requirement():
32+
33+
content = "name: my_env\n" \
34+
"dependencies:\n" \
35+
" - gevent=1.2.1\n" \
36+
" - pip:\n" \
37+
" - in=vali===d{}{}{"
38+
dep_file = parse(content, file_type=filetypes.conda_yml)
39+
assert len(dep_file.dependencies) == 0
40+
41+
1042
def test_conda_file_invalid_yml():
1143

1244
content = "wawth:dda : awd:\ndlll"

0 commit comments

Comments
 (0)