77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10+ import logging
1011import os
1112import uuid
1213from fnmatch import fnmatchcase
13- import logging
1414
1515import attr
1616from packageurl import normalize_qualifiers
2626from commoncode .datautils import String
2727from commoncode .fileutils import as_posixpath
2828from commoncode .resource import Resource
29- from typecode import contenttype
29+ try :
30+ from typecode import contenttype
31+ except ImportError :
32+ contenttype = None
33+
34+ try :
35+ from packagedcode import licensing
36+ except ImportError :
37+ licensing = None
3038
3139"""
3240This module contain data models for package and dependencies, abstracting and
@@ -366,6 +374,11 @@ class DependentPackage(ModelMixin):
366374 'lockfiles for Composer or Cargo contain extra dependency data.'
367375 )
368376
377+ extra_data = Mapping (
378+ label = 'extra data' ,
379+ help = 'A mapping of arbitrary extra data.' ,
380+ )
381+
369382
370383@attr .attributes (slots = True )
371384class Dependency (DependentPackage ):
@@ -771,7 +784,13 @@ def compute_normalized_license(declared_license, expression_symbols=None):
771784 if not declared_license :
772785 return
773786
774- from packagedcode import licensing
787+ if not licensing :
788+ if TRACE :
789+ logger_debug (
790+ f'Failed to compute license for { declared_license !r} : '
791+ 'cannot import packagedcode.licensing' )
792+ return 'unknown'
793+
775794 try :
776795 return licensing .get_normalized_expression (
777796 query_string = declared_license ,
@@ -781,7 +800,6 @@ def compute_normalized_license(declared_license, expression_symbols=None):
781800 # we never fail just for this
782801 if TRACE :
783802 logger_debug (f'Failed to compute license for { declared_license !r} : { e !r} ' )
784- # FIXME: add logging
785803 return 'unknown'
786804
787805
@@ -852,7 +870,8 @@ def is_datafile(cls, location, filetypes=tuple(), _bare_filename=False):
852870 filetypes = filetypes or cls .filetypes
853871 if not filetypes :
854872 return True
855- else :
873+ # we check for contenttype IFF this is available
874+ if contenttype :
856875 T = contenttype .get_type (location )
857876 actual_type = T .filetype_file .lower ()
858877 return any (ft in actual_type for ft in filetypes )
@@ -1207,6 +1226,13 @@ def __attrs_post_init__(self, *args, **kwargs):
12071226 def to_dict (self ):
12081227 return super ().to_dict (with_details = False )
12091228
1229+ def to_package_data (self ):
1230+ mapping = super ().to_dict (with_details = True )
1231+ mapping .pop ('package_uid' , None )
1232+ mapping .pop ('datafile_paths' , None )
1233+ mapping .pop ('datasource_ids' , None )
1234+ return PackageData .from_dict (mapping )
1235+
12101236 @classmethod
12111237 def from_package_data (cls , package_data , datafile_path ):
12121238 """
@@ -1255,7 +1281,15 @@ def is_compatible(self, package_data, include_qualifiers=True):
12551281 and self .primary_language == package_data .primary_language
12561282 )
12571283
1258- def update (self , package_data , datafile_path , replace = False ):
1284+ def update (
1285+ self ,
1286+ package_data ,
1287+ datafile_path ,
1288+ replace = False ,
1289+ include_version = True ,
1290+ include_qualifiers = False ,
1291+ include_subpath = False ,
1292+ ):
12591293 """
12601294 Update this Package with data from the ``package_data`` PackageData.
12611295
@@ -1281,9 +1315,15 @@ def update(self, package_data, datafile_path, replace=False):
12811315 if isinstance (package_data , dict ):
12821316 package_data = PackageData .from_dict (package_data )
12831317
1284- if not self .is_compatible (package_data , include_qualifiers = False ):
1318+ if not is_compatible (
1319+ purl1 = self ,
1320+ purl2 = package_data ,
1321+ include_version = include_version ,
1322+ include_qualifiers = include_qualifiers ,
1323+ include_subpath = include_subpath ,
1324+ ):
12851325 if TRACE_UPDATE :
1286- logger_debug (f'update: { self .purl } not compatible with: { package_data .purl } ' )
1326+ logger_debug (f'update: skipping: { self .purl } is not compatible with: { package_data .purl } ' )
12871327 return False
12881328
12891329 # always append these new items
@@ -1345,6 +1385,56 @@ def get_packages_files(self, codebase):
13451385 yield resource
13461386
13471387
1388+ def is_compatible (
1389+ purl1 ,
1390+ purl2 ,
1391+ include_version = True ,
1392+ include_qualifiers = True ,
1393+ include_subpath = True ,
1394+ ):
1395+ """
1396+ Return True if the ``purl1`` PackageURL-like object is compatible with
1397+ the ``purl2`` PackageURL-like object, e.g. it is about the same package.
1398+ PackageData objectys are PackageURL-like.
1399+
1400+ For example::
1401+ >>> p1 = PackageURL.from_string('pkg:deb/libncurses5@6.1-1ubuntu1.18.04?arch=arm64')
1402+ >>> p2 = PackageURL.from_string('pkg:deb/libncurses5@6.1-1ubuntu1.18.04')
1403+ >>> p3 = PackageURL.from_string('pkg:deb/libssl')
1404+ >>> p4 = PackageURL.from_string('pkg:deb/libncurses5')
1405+ >>> p5 = PackageURL.from_string('pkg:deb/libncurses5@6.1-1ubuntu1.18.04?arch=arm64#/sbin')
1406+ >>> is_compatible(p1, p2)
1407+ False
1408+ >>> is_compatible(p1, p2, include_qualifiers=False)
1409+ True
1410+ >>> is_compatible(p1, p4)
1411+ False
1412+ >>> is_compatible(p1, p4, include_version=False, include_qualifiers=False)
1413+ True
1414+ >>> is_compatible(p3, p4)
1415+ False
1416+ >>> is_compatible(p1, p5)
1417+ False
1418+ >>> is_compatible(p1, p5, include_subpath=False)
1419+ True
1420+ """
1421+ is_compatible = (
1422+ purl1 .type == purl2 .type
1423+ and purl1 .namespace == purl2 .namespace
1424+ and purl1 .name == purl2 .name
1425+ )
1426+ if include_version :
1427+ is_compatible = is_compatible and (purl1 .version == purl2 .version )
1428+
1429+ if include_qualifiers :
1430+ is_compatible = is_compatible and (purl1 .qualifiers == purl2 .qualifiers )
1431+
1432+ if include_subpath :
1433+ is_compatible = is_compatible and (purl1 .subpath == purl2 .subpath )
1434+
1435+ return is_compatible
1436+
1437+
13481438@attr .attributes (slots = True )
13491439class PackageWithResources (Package ):
13501440 """
@@ -1384,7 +1474,15 @@ def merge_sequences(list1, list2, **kwargs):
13841474 merged = []
13851475 existing = set ()
13861476 for item in list1 + list2 :
1387- key = item .to_tuple (** kwargs )
1477+ try :
1478+ if hasattr (item , 'to_tuple' ):
1479+ key = item .to_tuple (** kwargs )
1480+ else :
1481+ key = to_tuple (kwargs )
1482+
1483+ except Exception as e :
1484+ raise Exception (f'Failed to merge sequences: { item } ' , f'kwargs: { kwargs } ' ) from e
1485+
13881486 if not key in existing :
13891487 merged .append (item )
13901488 existing .add (key )
0 commit comments