Skip to content

Commit d06bbea

Browse files
committed
Complete switch to new PyPI parsers
Now using dparse2, pkginfo2 and pip-requirement-parser This addresses a security vulnerability in dparse. Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent bd5cb78 commit d06bbea

6 files changed

Lines changed: 111 additions & 49 deletions

File tree

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ parameter-expansion-patched==0.2.1b4
4646
pdfminer.six==20201018
4747
pefile==2021.5.24
4848
pkginfo2==30.0.0
49-
pip-requirements-parser==31.0.1
49+
pip-requirements-parser==31.2.0
5050
pluggy==0.13.1
5151
plugincode==21.1.21
5252
ply==3.11

setup-mini.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ install_requires =
8787
pdfminer.six >= 20200101
8888
pefile >= 2020.1.1
8989
pkginfo2 >= 30.0.0
90-
pip-requirements-parser >= 31.0.0
90+
pip-requirements-parser >= 31.2.0
9191
pluggy >= 0.12.0
9292
plugincode >= 21.1.21
9393
publicsuffix2

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ install_requires =
8787
pdfminer.six >= 20200101
8888
pefile >= 2020.1.1
8989
pkginfo2 >= 30.0.0
90-
pip-requirements-parser >= 31.0.0
90+
pip-requirements-parser >= 31.2.0
9191
pluggy >= 0.12.0
9292
plugincode >= 21.1.21
9393
publicsuffix2

src/packagedcode/pypi.py

Lines changed: 106 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
from pathlib import Path
2121

2222
import attr
23-
import dparse
23+
import dparse2
24+
import pip_requirements_parser
25+
import pkginfo2
2426
from packageurl import PackageURL
2527
from packaging.requirements import Requirement
2628
from packaging import markers
2729
from packaging.utils import canonicalize_name
2830

29-
# TODO: replace this
30-
from pkginfo import SDist
31-
3231
from commoncode import filetype
3332
from commoncode import fileutils
3433
from packagedcode import models
@@ -119,7 +118,7 @@ def recognize(cls, location):
119118
package archive, manifest or similar.
120119
"""
121120
yield parse_metadata(cls, location)
122-
121+
123122

124123
def parse_metadata(cls, location):
125124
"""
@@ -213,7 +212,7 @@ def recognize(cls, location):
213212
# FIXME: handle other_urls
214213

215214
try:
216-
sdist = SDist(location)
215+
sdist = pkginfo2.SDist(location)
217216
except ValueError:
218217
return
219218
urls, other_urls = get_urls(sdist)
@@ -295,11 +294,11 @@ def recognize(cls, location):
295294
"""
296295
file_name = fileutils.file_name(location)
297296

298-
dependency_type = get_dparse_dependency_type(file_name)
297+
dependency_type = get_dparse2_supported_file_name(file_name)
299298
if not dependency_type:
300299
return
301300

302-
dependent_packages = parse_with_dparse(
301+
dependent_packages = parse_with_dparse2(
303302
location=location,
304303
dependency_type=dependency_type,
305304
)
@@ -336,9 +335,9 @@ def recognize(cls, location):
336335
if name == 'hash':
337336
sha256 = meta.get('sha256')
338337

339-
dependent_packages = parse_with_dparse(
338+
dependent_packages = parse_with_dparse2(
340339
location=location,
341-
dependency_type=dparse.filetypes.pipfile_lock,
340+
file_name='Pipfile.lock',
342341
)
343342
yield cls(sha256=sha256, dependencies=dependent_packages)
344343

@@ -347,10 +346,13 @@ def recognize(cls, location):
347346
class RequirementsFile(PythonPackage, models.PackageManifest):
348347

349348
file_patterns = (
350-
'*requirements*.txt',
351-
'*requirements*.pip',
352-
'*requirements*.in',
349+
'*requirement*.txt',
350+
'*requirement*.pip',
351+
'*requirement*.in',
353352
'requires.txt',
353+
'requirements/*.txt',
354+
'requirements/*.pip',
355+
'requirements/*.in',
354356
)
355357

356358
@classmethod
@@ -366,12 +368,64 @@ def recognize(cls, location):
366368
Yield one or more Package manifest objects given a file ``location`` pointing to a
367369
package archive, manifest or similar.
368370
"""
369-
dependent_packages = parse_with_dparse(
370-
location=location,
371-
dependency_type=dparse.filetypes.requirements_txt
371+
dependencies = get_requirements_txt_dependencies(location=location)
372+
yield cls(dependencies=dependencies)
373+
374+
375+
def get_requirements_txt_dependencies(location):
376+
"""
377+
Return a list of DependentPackage found in a requirements file at
378+
``location`` or an empty list.
379+
"""
380+
req_file = pip_requirements_parser.RequirementsFile.from_file(
381+
filename=location,
382+
include_nested=False,
383+
)
384+
if not req_file or not req_file.requirements:
385+
return []
386+
387+
dependent_packages = []
388+
389+
# for now we ignore plain options and errors
390+
for req in req_file.requirements:
391+
392+
if req.name:
393+
# will be None if not pinned
394+
version = req.get_pinned_version
395+
purl = PackageURL(type='pypi', name=req.name, version=version)
396+
397+
else:
398+
# this is odd, but this can be null
399+
purl = None
400+
401+
purl = purl and purl.to_string() or None
402+
403+
if req.is_editable:
404+
requirement = req.dumps()
405+
else:
406+
requirement = req.dumps(with_name=False)
407+
408+
if location.endswith(('dev.txt', 'test.txt', 'tests.txt',)):
409+
scope = 'development'
410+
is_runtime = False
411+
is_optional = True
412+
else:
413+
scope = 'install'
414+
is_runtime = True
415+
is_optional = False
416+
417+
dependent_packages.append(
418+
models.DependentPackage(
419+
purl=purl,
420+
scope=scope,
421+
is_runtime=is_runtime,
422+
is_optional=is_optional,
423+
is_resolved=req.is_pinned or False,
424+
requirement=requirement
425+
)
372426
)
373-
yield cls(dependencies=dependent_packages)
374427

428+
return dependent_packages
375429

376430

377431
def get_attribute(metainfo, name, multiple=False):
@@ -702,6 +756,8 @@ def is_requirements_file(location):
702756
True
703757
>>> is_requirements_file('requirements.txt')
704758
True
759+
>>> is_requirements_file('requirement.txt')
760+
True
705761
>>> is_requirements_file('requirements.in')
706762
True
707763
>>> is_requirements_file('requirements.pip')
@@ -710,53 +766,59 @@ def is_requirements_file(location):
710766
True
711767
>>> is_requirements_file('some-requirements-dev.txt')
712768
True
713-
>>> is_requirements_file('reqs.txt')
714-
False
715769
>>> is_requirements_file('requires.txt')
716770
True
771+
>>> is_requirements_file('requirements/base.txt')
772+
True
773+
>>> is_requirements_file('reqs.txt')
774+
False
717775
"""
718776
filename = fileutils.file_name(location)
719777
req_files = (
720-
'*requirements*.txt',
721-
'*requirements*.pip',
722-
'*requirements*.in',
778+
'*requirement*.txt',
779+
'*requirement*.pip',
780+
'*requirement*.in',
723781
'requires.txt',
724782
)
725-
return any(fnmatch.fnmatchcase(filename, rf) for rf in req_files)
783+
is_req = any(fnmatch.fnmatchcase(filename, rf) for rf in req_files)
784+
if is_req:
785+
return True
726786

787+
parent = fileutils.parent_directory(location)
788+
parent_name = fileutils.file_name(parent)
789+
pip_extensions = ('.txt', 'pip', '.in',)
790+
return parent_name == 'requirements' and filename.endswith(pip_extensions)
727791

728-
def get_dparse_dependency_type(file_name):
792+
793+
def get_dparse2_supported_file_name(file_name):
729794
"""
730-
Return the type of a dependency as a string or None given a `file_name`
795+
Return the file_name if this is supported or None given a `file_name`
731796
string.
732797
"""
733798
# this is kludgy but the upstream data structure and API needs this
734-
filetype_by_name_end = {
735-
'Pipfile.lock': dparse.filetypes.pipfile_lock,
736-
'Pipfile': dparse.filetypes.pipfile,
737-
'conda.yml': dparse.filetypes.conda_yml,
738-
'setup.cfg': dparse.filetypes.setup_cfg,
739-
'tox.ini': dparse.filetypes.tox_ini,
740-
}
741-
742-
for extensions, dependency_type in filetype_by_name_end.items():
743-
if file_name.endswith(extensions):
744-
return dependency_type
799+
dfile_names = (
800+
'Pipfile.lock',
801+
'Pipfile',
802+
'conda.yml',
803+
'setup.cfg',
804+
'tox.ini',
805+
)
745806

746-
if is_requirements_file(file_name):
747-
return dparse.filetypes.requirements_txt
807+
for dfile_name in dfile_names:
808+
if file_name.endswith(dfile_name):
809+
return file_name
748810

749811

750-
def parse_with_dparse(location, dependency_type=None):
812+
def parse_with_dparse2(location, file_name=None):
751813
"""
752-
Return a list of DependentPackage built from a dparse-supported dependency
753-
manifest such as requirements.txt, Conda manifest or Pipfile.lock files, or
754-
return an empty list.
814+
Return a list of DependentPackage built from a dparse2-supported dependency
815+
manifest such as Conda manifest or Pipfile.lock files, or return an empty
816+
list.
755817
"""
756818
with open(location) as f:
757819
content = f.read()
758820

759-
dep_file = dparse.parse(content, file_type=dependency_type)
821+
dep_file = dparse2.parse(content, file_name=file_name)
760822
if not dep_file:
761823
return []
762824

@@ -767,7 +829,7 @@ def parse_with_dparse(location, dependency_type=None):
767829
is_resolved = False
768830
purl = PackageURL(type='pypi', name=dependency.name)
769831

770-
# note: dparse.dependencies.Dependency.specs comes from
832+
# note: dparse2.dependencies.Dependency.specs comes from
771833
# packaging.requirements.Requirement.specifier
772834
# which in turn is a packaging.specifiers.SpecifierSet objects
773835
# and a SpecifierSet._specs is a set of either:

tests/packagedcode/data/plugin/help.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ Package: pypi
354354
--------------------------------------------
355355
Package: pypi
356356
class: packagedcode.pypi:RequirementsFile
357-
file_patterns: *requirements*.txt, *requirements*.pip, *requirements*.in, requires.txt
357+
file_patterns: *requirement*.txt, *requirement*.pip, *requirement*.in, requires.txt, requirements/*.txt, requirements/*.pip, requirements/*.in
358358

359359
--------------------------------------------
360360
Package: readme

tests/packagedcode/test_pypi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def test_parse_dependency_file_with_invalid_does_not_fail(self):
365365
test_file = self.get_test_loc('pypi/requirements_txt/invalid_spec/requirements.txt')
366366
package = pypi.RequirementsFile.recognize(test_file)
367367
expected_loc = self.get_test_loc('pypi/requirements_txt/invalid_spec/output.expected.json')
368-
self.check_packages(package, expected_loc, regen=True)
368+
self.check_packages(package, expected_loc, regen=False)
369369

370370

371371
class TestPyPiPipfile(PackageTester):

0 commit comments

Comments
 (0)