Skip to content

Commit 6dab7e1

Browse files
authored
Merge pull request #31 from nexB/supported_wheels_failure
Add sdist support Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
2 parents db336bd + 065bdce commit 6dab7e1

17 files changed

Lines changed: 3626 additions & 73 deletions

src/_packagedcode/pypi.py

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@
99
#
1010

1111
import ast
12+
from configparser import ConfigParser
1213
import json
1314
import logging
15+
from pathlib import Path
1416
import os
1517
import re
1618
import sys
19+
from typing import NamedTuple
20+
import tempfile
1721
import zipfile
18-
from configparser import ConfigParser
19-
from pathlib import Path
2022

2123
import dparse2
24+
import packaging
2225
import pip_requirements_parser
2326
import pkginfo2
2427
from commoncode import fileutils
28+
from packaging.specifiers import SpecifierSet
2529
from packageurl import PackageURL
2630
from packaging import markers
2731
from 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+
648661
class 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

738817
class 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

src/python_inspector/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
# See https://github.com/nexB/scancode-toolkit for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
9+
10+
DEFAULT_PYTHON_VERSION = "3.8"

0 commit comments

Comments
 (0)