2020from pathlib import Path
2121
2222import attr
23- import dparse
23+ import dparse2
24+ import pip_requirements_parser
25+ import pkginfo2
2426from packageurl import PackageURL
2527from packaging .requirements import Requirement
2628from packaging import markers
2729from packaging .utils import canonicalize_name
2830
29- # TODO: replace this
30- from pkginfo import SDist
31-
3231from commoncode import filetype
3332from commoncode import fileutils
3433from 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
124123def 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):
347346class 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
377431def 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:
0 commit comments