Skip to content

Commit fc4ada9

Browse files
Add support for pip-inspect.deplock files
Add parser for pip-inspect.deplock files generated by deplock which has all the package metadata, i.e. the resolved versions and the dependency relationships. Reference: aboutcode-org/scancode.io#1262 Signed-off-by: Ayan Sinha Mahapatra <[email protected]>
1 parent c8046f1 commit fc4ada9

File tree

6 files changed

+2291
-1
lines changed

6 files changed

+2291
-1
lines changed

src/packagedcode/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@
207207
windows.MicrosoftUpdateManifestHandler,
208208

209209
win_pe.WindowsExecutableHandler,
210+
211+
# These are handlers for deplock generated files
212+
pypi.PipInspectDeplockHandler,
210213
]
211214

212215
if on_linux:

src/packagedcode/pypi.py

+120-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from packagedcode.utils import parse_maintainer_name_email
4343
from packagedcode.utils import yield_dependencies_from_package_data
4444
from packagedcode.utils import yield_dependencies_from_package_resource
45+
from packagedcode.utils import get_base_purl
4546

4647
try:
4748
from zipfile import Path as ZipPath
@@ -563,6 +564,123 @@ def parse(cls, location, package_only=False):
563564
yield models.PackageData.from_data(package_data, package_only)
564565

565566

567+
class PipInspectDeplockHandler(models.DatafileHandler):
568+
datasource_id = 'pypi_inspect_deplock'
569+
path_patterns = ('*pip-inspect.deplock',)
570+
default_package_type = 'pypi'
571+
default_primary_language = 'Python'
572+
description = 'Python poetry pyproject.toml'
573+
# These are files generated by deplock, see https://github.com/nexB/dependency-inspector
574+
documentation_url = 'https://pip.pypa.io/en/stable/cli/pip_inspect/'
575+
576+
@classmethod
577+
def get_resolved_package_from_metadata(cls, metadata, package_only=False):
578+
579+
requires_dist = metadata.get('requires_dist')
580+
dependencies_for_resolved = get_requires_dependencies(
581+
requires=requires_dist,
582+
)
583+
package_data = dict(
584+
datasource_id=cls.datasource_id,
585+
type=cls.default_package_type,
586+
primary_language='Python',
587+
name=metadata.get('name'),
588+
version=metadata.get('version'),
589+
extracted_license_statement=metadata.get('license'),
590+
description=metadata.get('description'),
591+
keywords=metadata.get('keywords'),
592+
is_virtual=True,
593+
dependencies=[
594+
dep.to_dict()
595+
for dep in dependencies_for_resolved
596+
],
597+
)
598+
return models.PackageData.from_data(package_data, package_only)
599+
600+
@classmethod
601+
def parse(cls, location, package_only=False):
602+
603+
with open(location) as f:
604+
content = f.read()
605+
606+
data = json.loads(content)
607+
installed_packages = data.get('installed')
608+
if not installed_packages:
609+
return
610+
611+
main_package_metadata = {}
612+
dependencies = []
613+
614+
direct_deps_of_main_package = []
615+
616+
for package_metadata in installed_packages:
617+
package_metadata_dep = package_metadata.get('metadata')
618+
619+
# `direct_url` is only present for root package
620+
# `requested` is true for root package and direct dependencies only
621+
if package_metadata.get('requested') and 'direct_url' in package_metadata:
622+
main_package_metadata = package_metadata_dep
623+
main_package_requires = main_package_metadata.get('requires_dist')
624+
dependencies_for_main = get_requires_dependencies(
625+
requires=main_package_requires,
626+
)
627+
direct_deps_of_main_package.extend([
628+
get_base_purl(dep.purl)
629+
for dep in dependencies_for_main
630+
])
631+
continue
632+
633+
package_data_dep = cls.get_resolved_package_from_metadata(
634+
metadata=package_metadata_dep,
635+
package_only=package_only,
636+
)
637+
dep_purl = package_data_dep.purl
638+
dependency = models.DependentPackage(
639+
purl=dep_purl,
640+
extracted_requirement=None,
641+
scope=None,
642+
is_runtime=True,
643+
is_optional=False,
644+
is_direct=False,
645+
is_resolved=True,
646+
resolved_package=package_data_dep.to_dict()
647+
)
648+
dependencies.append(dependency)
649+
650+
dependency_mappings = []
651+
resolved_main_dependencies = []
652+
653+
# Update is_direct for direct dependencies
654+
for dep in dependencies:
655+
base_purl = get_base_purl(dep.purl)
656+
if base_purl in direct_deps_of_main_package:
657+
dep.is_direct = True
658+
resolved_main_dependencies.append(base_purl)
659+
660+
dependency_mappings.append(dep.to_dict())
661+
662+
pip_version = data.get('pip_version')
663+
inspect_version = data.get('version')
664+
extra_data = {
665+
"pip_version": pip_version,
666+
"inspect_version": inspect_version,
667+
}
668+
669+
package_data_main = cls.get_resolved_package_from_metadata(
670+
metadata=main_package_metadata,
671+
package_only=package_only,
672+
)
673+
674+
main_dependencies = []
675+
for dep in package_data_main.dependencies:
676+
base_purl = get_base_purl(purl=dep.get('purl'))
677+
if base_purl not in resolved_main_dependencies:
678+
main_dependencies.append(dep)
679+
680+
package_data_main.dependencies = dependencies
681+
package_data_main.dependencies.extend(main_dependencies)
682+
package_data_main.extra_data = extra_data
683+
yield package_data_main
566684

567685

568686
META_DIR_SUFFIXES = '.dist-info', '.egg-info', 'EGG-INFO',
@@ -1494,7 +1612,7 @@ def get_dist_dependencies(dist):
14941612
return get_requires_dependencies(requires=dist.requires)
14951613

14961614

1497-
def get_requires_dependencies(requires, default_scope='install'):
1615+
def get_requires_dependencies(requires, default_scope='install', is_direct=True):
14981616
"""
14991617
Return a list of DependentPackage found in a ``requires`` list of
15001618
requirement strings or an empty list.
@@ -1539,6 +1657,7 @@ def get_requires_dependencies(requires, default_scope='install'):
15391657
is_runtime=True,
15401658
is_optional=True if bool(extra) else False,
15411659
is_resolved=is_resolved,
1660+
is_direct=is_direct,
15421661
extracted_requirement=str(req),
15431662
))
15441663

tests/packagedcode/data/plugin/help.txt

+7
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,13 @@ Package type: pypi
713713
description: PyPI extracted egg PKG-INFO
714714
path_patterns: '*/EGG-INFO/PKG-INFO'
715715
--------------------------------------------
716+
Package type: pypi
717+
datasource_id: pypi_inspect_deplock
718+
documentation URL: https://pip.pypa.io/en/stable/cli/pip_inspect/
719+
primary language: Python
720+
description: Python poetry pyproject.toml
721+
path_patterns: '*pip-inspect.deplock'
722+
--------------------------------------------
716723
Package type: pypi
717724
datasource_id: pypi_poetry_pyproject_toml
718725
documentation URL: https://packaging.python.org/en/latest/specifications/pyproject-toml/

tests/packagedcode/data/pypi/deplock/univers/pip-inspect.deplock

+519
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)