Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 117 additions & 15 deletions src/_packagedcode/pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
import os
import re
import sys
from typing import NamedTuple
import zipfile
from configparser import ConfigParser
from pathlib import Path

import dparse2
import packaging
import pip_requirements_parser
import pkginfo2
from commoncode import fileutils
from packaging.specifiers import SpecifierSet
from packageurl import PackageURL
from packaging import markers
from packaging.requirements import Requirement
Expand Down Expand Up @@ -453,7 +456,8 @@ def parse_metadata(location, datasource_id, package_type):
primary_language='Python',
name=name,
version=version,
description=get_description(meta, location),
description=get_description( metainfo= meta, location= str(location)),
Comment thread
TG1999 marked this conversation as resolved.
Outdated
#TODO: https://github.com/nexB/scancode-toolkit/issues/3014
declared_license=get_declared_license(meta),
keywords=get_keywords(meta),
parties=get_parties(meta),
Expand Down Expand Up @@ -645,6 +649,14 @@ def parse(cls, location):
)


class ResolvedPurl(NamedTuple):
"""
A resolved PURL
"""
purl: PackageURL
is_resolved: bool


class BaseDependencyFileHandler(BasePypiHandler):
"""
Base class for a dependency files parsed with the same library
Expand Down Expand Up @@ -684,10 +696,44 @@ def parse(cls, location):

metadata = {}
parser = ConfigParser()
dependent_packages = []
with open(location) as f:
parser.read_file(f)

for section in parser.values():
if section.name == 'options':

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You likely want to add also the legacy "setup_requires" and "test_requires" as well as the "python_requires"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pombredanne can you provide me some sample setup.cfg file with legacy "setup_requires", "test_requires", "python_requires"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://github.com/search?l=INI&q="setup_requires"&type=Code and https://github.com/karpierz/QQt/blob/930e22d9612f814fa242b1547ad037037ef6b5c7/setup.cfg
And also https://github.com/search?l=INI&q="test_requires"&type=Code for test_requires

Other notes:

  • use python as a scope for the "python_requires" and use a Package URL of pkg:generic/python such that this is clear that this is not a PyPI package. That way you can spot this special package and scope as needed to use it in your processing
  • make sure these fixes are also applied to setup.py parsing as well as other PyPI metadata ... the important part is mostly for python_requires ... setup/test requires are legacy and could only be seen in setup.py/setup.cfg
  • you need tests also on the SCTK side... you may want to start there instead
  • for setup_requires, use setup as scope.

scope_by_sub_section = {
"install_requires": "install",
"tests_require": "test",
"setup_requires": "setup",
"python_requires": "python",
}
for sub_section in scope_by_sub_section:
Comment thread
TG1999 marked this conversation as resolved.
Outdated
if sub_section not in section:
continue
scope = scope_by_sub_section[sub_section]
if scope != "python":
reqs = list(get_requirement_from_section(section=section, sub_section=sub_section))
dependent_packages.extend(cls.parse_reqs(reqs, scope))
continue
python_requires = section[sub_section]
purl = PackageURL(
name="python",
type="generic"
Comment thread
TG1999 marked this conversation as resolved.
Outdated
)
resolved_purl = is_purl_resolved(purl = purl, specifiers= SpecifierSet(python_requires))
dependent_packages.append(models.DependentPackage(
purl=str(resolved_purl.purl),
scope=scope,
is_runtime=True,
is_optional=False,
is_resolved=resolved_purl.is_resolved,
extracted_requirement=f"python_requires{python_requires}",
Comment thread
TG1999 marked this conversation as resolved.
Outdated
))

if section.name == "options.extras_require":
for sub_section in section:
reqs = list(get_requirement_from_section(section=section, sub_section=sub_section))
dependent_packages.extend(cls.parse_reqs(reqs, sub_section))
if section.name == 'metadata':
options = (
'name',
Expand Down Expand Up @@ -715,14 +761,7 @@ def parse(cls, location):
)
]

dependency_type = get_dparse2_supported_file_name(file_name)
if not dependency_type:
return

dependencies = parse_with_dparse2(
Comment thread
TG1999 marked this conversation as resolved.
location=location,
file_name=dependency_type,
)
yield models.PackageData(
datasource_id=cls.datasource_id,
type=cls.default_package_type,
Expand All @@ -731,9 +770,48 @@ def parse(cls, location):
parties=parties,
homepage_url=metadata.get('url'),
primary_language=cls.default_primary_language,
dependencies=dependencies,
dependencies=dependent_packages,
)

@classmethod
def parse_reqs(cls, reqs, scope):
"""
Parse a list of requirements and return a list of dependencies
"""
dependent_packages = []
Comment thread
TG1999 marked this conversation as resolved.
for req in reqs:
req_parsed = packaging.requirements.Requirement(str(req))
name = canonicalize_name(req_parsed.name)
purl = PackageURL(type="pypi", name=name)
specifiers = req_parsed.specifier._specs
resolved_purl = is_purl_resolved(purl = purl, specifiers= specifiers)
Comment thread
TG1999 marked this conversation as resolved.
Outdated
dependent_packages.append(
models.DependentPackage(
purl=str(resolved_purl.purl),
scope=scope,
is_runtime=True,
is_optional=False,
is_resolved=resolved_purl.is_resolved,
extracted_requirement=req
)
)
return dependent_packages


def is_purl_resolved(purl: PackageURL, specifiers: SpecifierSet):
Comment thread
TG1999 marked this conversation as resolved.
Outdated
"""
Check if the purl is resolved
Comment thread
TG1999 marked this conversation as resolved.
Outdated
"""
is_resolved = False
if len(specifiers) == 1:
specifier = list(specifiers)[0]
if specifier.operator in ('==', '==='):
is_resolved = True
purl = purl._replace(version=specifier.version)
return ResolvedPurl(
purl=purl,
is_resolved=is_resolved,
)

class PipfileHandler(BaseDependencyFileHandler):
datasource_id = 'pipfile'
Expand Down Expand Up @@ -1017,10 +1095,11 @@ def get_classifiers(metainfo):
license_classifiers = []
other_classifiers = []
for classifier in classifiers:
if classifier.startswith('License'):
license_classifiers.append(classifier)
else:
other_classifiers.append(classifier)
if classifier:
if classifier.startswith('License'):
license_classifiers.append(classifier)
else:
other_classifiers.append(classifier)
return license_classifiers, other_classifiers


Expand Down Expand Up @@ -1248,7 +1327,6 @@ def get_dparse2_supported_file_name(file_name):
'Pipfile.lock',
'Pipfile',
'conda.yml',
'setup.cfg',
)

for dfile_name in dfile_names:
Expand Down Expand Up @@ -1897,3 +1975,27 @@ def compute_normalized_license(declared_license):

if detected_licenses:
return combine_expressions(detected_licenses)


def get_requirement_from_section(section, sub_section):
Comment thread
TG1999 marked this conversation as resolved.
"""
Generate requirements from the `sub_section`
Comment thread
TG1999 marked this conversation as resolved.
Outdated
"""
content = section.get(sub_section) or ""
for req in content.splitlines():
Comment thread
TG1999 marked this conversation as resolved.
Outdated
if not req:
continue
#pytest-mypy >= 0.9.1; \
req = req.replace("; \\", "")
Comment thread
TG1999 marked this conversation as resolved.
Outdated
# pip>=19.1 # For proper file:// URLs support.
if "#" in req:
req , _ = req.rsplit("#")
try:
Requirement(req)
Comment thread
TG1999 marked this conversation as resolved.
Outdated
yield req
except:
#pure-eval; black; tox
Comment thread
TG1999 marked this conversation as resolved.
Outdated
req_split_by_semi_colon = req.split(";")
Comment thread
TG1999 marked this conversation as resolved.
Outdated
req_split_by_semi_colon = [req.strip() for req in req_split_by_semi_colon if req]
for req in req_split_by_semi_colon:
yield req
2 changes: 2 additions & 0 deletions src/python_inspector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
# See https://github.com/nexB/scancode-toolkit for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#

DEFAULT_PYTHON_VERSION = "3.8"
Loading