99#
1010
1111import ast
12+ from configparser import ConfigParser
1213import json
1314import logging
15+ from pathlib import Path
1416import os
1517import re
1618import sys
19+ from typing import NamedTuple
20+ import tempfile
1721import zipfile
18- from configparser import ConfigParser
19- from pathlib import Path
2022
2123import dparse2
24+ import packaging
2225import pip_requirements_parser
2326import pkginfo2
2427from commoncode import fileutils
28+ from packaging .specifiers import SpecifierSet
2529from packageurl import PackageURL
2630from packaging import markers
2731from packaging .requirements import Requirement
@@ -453,7 +457,8 @@ def parse_metadata(location, datasource_id, package_type):
453457 primary_language = 'Python' ,
454458 name = name ,
455459 version = version ,
456- description = get_description (meta , location ),
460+ description = get_description (metainfo = meta , location = str (location )),
461+ #TODO: https://github.com/nexB/scancode-toolkit/issues/3014
457462 declared_license = get_declared_license (meta ),
458463 keywords = get_keywords (meta ),
459464 parties = get_parties (meta ),
@@ -645,6 +650,14 @@ def parse(cls, location):
645650 )
646651
647652
653+ class ResolvedPurl (NamedTuple ):
654+ """
655+ A resolved PURL
656+ """
657+ purl : PackageURL
658+ is_resolved : bool
659+
660+
648661class BaseDependencyFileHandler (BasePypiHandler ):
649662 """
650663 Base class for a dependency files parsed with the same library
@@ -684,10 +697,43 @@ def parse(cls, location):
684697
685698 metadata = {}
686699 parser = ConfigParser ()
700+ dependent_packages = []
687701 with open (location ) as f :
688702 parser .read_file (f )
689-
690703 for section in parser .values ():
704+ if section .name == 'options' :
705+ scope_by_sub_section = {
706+ "install_requires" : "install" ,
707+ "tests_require" : "test" ,
708+ "setup_requires" : "setup" ,
709+ "python_requires" : "python" ,
710+ }
711+ for sub_section , scope in scope_by_sub_section .items ():
712+ if sub_section not in section :
713+ continue
714+ if scope != "python" :
715+ reqs = list (get_requirement_from_section (section = section , sub_section = sub_section ))
716+ dependent_packages .extend (cls .parse_reqs (reqs , scope ))
717+ continue
718+ python_requires_specifier = section [sub_section ]
719+ purl = PackageURL (
720+ type = "generic" ,
721+ name = "python" ,
722+ )
723+ resolved_purl = get_resolved_purl (purl = purl , specifiers = SpecifierSet (python_requires_specifier ))
724+ dependent_packages .append (models .DependentPackage (
725+ purl = str (resolved_purl .purl ),
726+ scope = scope ,
727+ is_runtime = True ,
728+ is_optional = False ,
729+ is_resolved = resolved_purl .is_resolved ,
730+ extracted_requirement = f"python_requires{ python_requires_specifier } " ,
731+ ))
732+
733+ if section .name == "options.extras_require" :
734+ for sub_section in section :
735+ reqs = list (get_requirement_from_section (section = section , sub_section = sub_section ))
736+ dependent_packages .extend (cls .parse_reqs (reqs , sub_section ))
691737 if section .name == 'metadata' :
692738 options = (
693739 'name' ,
@@ -715,14 +761,7 @@ def parse(cls, location):
715761 )
716762 ]
717763
718- dependency_type = get_dparse2_supported_file_name (file_name )
719- if not dependency_type :
720- return
721764
722- dependencies = parse_with_dparse2 (
723- location = location ,
724- file_name = dependency_type ,
725- )
726765 yield models .PackageData (
727766 datasource_id = cls .datasource_id ,
728767 type = cls .default_package_type ,
@@ -731,9 +770,49 @@ def parse(cls, location):
731770 parties = parties ,
732771 homepage_url = metadata .get ('url' ),
733772 primary_language = cls .default_primary_language ,
734- dependencies = dependencies ,
773+ dependencies = dependent_packages ,
735774 )
736775
776+ @classmethod
777+ def parse_reqs (cls , reqs , scope ):
778+ """
779+ Parse a list of requirements and return a list of dependencies
780+ """
781+ dependent_packages = []
782+ for req in reqs :
783+ req_parsed = packaging .requirements .Requirement (str (req ))
784+ name = canonicalize_name (req_parsed .name )
785+ purl = PackageURL (type = "pypi" , name = name )
786+ specifiers = req_parsed .specifier ._specs
787+ resolved_purl = get_resolved_purl (purl = purl , specifiers = specifiers )
788+ dependent_packages .append (
789+ models .DependentPackage (
790+ purl = str (resolved_purl .purl ),
791+ scope = scope ,
792+ is_runtime = True ,
793+ is_optional = False ,
794+ is_resolved = resolved_purl .is_resolved ,
795+ extracted_requirement = req
796+ )
797+ )
798+ return dependent_packages
799+
800+
801+ def get_resolved_purl (purl : PackageURL , specifiers : SpecifierSet ):
802+ """
803+ Check if the purl is resolved and return a ResolvedPurl.
804+ If the purl is resolved, update its version to the pinned version
805+ """
806+ is_resolved = False
807+ if len (specifiers ) == 1 :
808+ specifier = list (specifiers )[0 ]
809+ if specifier .operator in ('==' , '===' ):
810+ is_resolved = True
811+ purl = purl ._replace (version = specifier .version )
812+ return ResolvedPurl (
813+ purl = purl ,
814+ is_resolved = is_resolved ,
815+ )
737816
738817class PipfileHandler (BaseDependencyFileHandler ):
739818 datasource_id = 'pipfile'
@@ -823,7 +902,7 @@ def get_requirements_txt_dependencies(location, include_nested=False):
823902 include_nested = include_nested ,
824903 )
825904 if not req_file or not req_file .requirements :
826- return []
905+ return [], {}
827906
828907 # for now we ignore errors
829908 extra_data = {}
@@ -1017,10 +1096,11 @@ def get_classifiers(metainfo):
10171096 license_classifiers = []
10181097 other_classifiers = []
10191098 for classifier in classifiers :
1020- if classifier .startswith ('License' ):
1021- license_classifiers .append (classifier )
1022- else :
1023- other_classifiers .append (classifier )
1099+ if classifier :
1100+ if classifier .startswith ('License' ):
1101+ license_classifiers .append (classifier )
1102+ else :
1103+ other_classifiers .append (classifier )
10241104 return license_classifiers , other_classifiers
10251105
10261106
@@ -1248,7 +1328,6 @@ def get_dparse2_supported_file_name(file_name):
12481328 'Pipfile.lock' ,
12491329 'Pipfile' ,
12501330 'conda.yml' ,
1251- 'setup.cfg' ,
12521331 )
12531332
12541333 for dfile_name in dfile_names :
@@ -1897,3 +1976,18 @@ def compute_normalized_license(declared_license):
18971976
18981977 if detected_licenses :
18991978 return combine_expressions (detected_licenses )
1979+
1980+
1981+ def get_requirement_from_section (section , sub_section ):
1982+ """
1983+ Yield extracted requirement from the ``sub_section`` key of of a ``section``
1984+ mapping (from a setup.cfg)
1985+ """
1986+ content = section .get (sub_section , "" )
1987+ temp = tempfile .NamedTemporaryFile (delete = False )
1988+ location = temp .name
1989+ with open (location , "w" ) as f :
1990+ f .write (content )
1991+ packages , _ = get_requirements_txt_dependencies (location = location )
1992+ for req in packages :
1993+ yield req .extracted_requirement
0 commit comments