@@ -104,6 +104,7 @@ class DetectionCategory(Enum):
104104 IMPERFECT_COVERAGE = 'imperfect-match-coverage'
105105 FALSE_POSITVE = 'possible-false-positive'
106106 UNDETECTED_LICENSE = 'undetected-license'
107+ MATCH_FRAGMENTS = 'match_fragments'
107108
108109
109110class DetectionRule (Enum ):
@@ -141,6 +142,9 @@ class FileRegion:
141142 start_line = attr .ib (type = int )
142143 end_line = attr .ib (type = int )
143144
145+ def to_dict (self ):
146+ return attr .asdict (self , dict_factory = dict )
147+
144148
145149@attr .s (slots = True , eq = False , order = False )
146150class LicenseDetection :
@@ -613,6 +617,106 @@ def from_dicts(cls, license_match_mappings):
613617 """
614618 return [LicenseMatchFromResult .from_dict (lmm ) for lmm in license_match_mappings ]
615619
620+ def to_dict (
621+ self ,
622+ include_text = False ,
623+ license_text_diagnostics = False ,
624+ whole_lines = True ,
625+ ):
626+ """
627+ Return a "result" scan data built from a LicenseMatch object.
628+ """
629+ matched_text = None
630+ if include_text :
631+ matched_text = self .matched_text
632+
633+ result = {}
634+
635+ # Detection Level Information
636+ result ['score' ] = self .score ()
637+ result ['start_line' ] = self .start_line
638+ result ['end_line' ] = self .end_line
639+ result ['matched_length' ] = self .len ()
640+ result ['match_coverage' ] = self .coverage ()
641+ result ['matcher' ] = self .matcher
642+
643+ # LicenseDB Level Information (Rule that was matched)
644+ result ['license_expression' ] = self .rule .license_expression
645+ result ['rule_identifier' ] = self .rule .identifier
646+ result ['rule_relevance' ] = self .rule .relevance
647+ result ['rule_url' ] = self .rule .rule_url
648+
649+ if include_text :
650+ result ['matched_text' ] = matched_text
651+ return result
652+
653+
654+ def collect_license_detections (codebase , include_license_clues = True ):
655+ """
656+ Return a list of LicenseDetectionFromResult from a ``codebase``
657+ """
658+ has_packages = hasattr (codebase .root , 'package_data' )
659+ has_licenses = hasattr (codebase .root , 'license_detections' )
660+
661+ all_license_detections = []
662+
663+ for resource in codebase .walk ():
664+
665+ resource_license_detections = []
666+ if has_licenses :
667+ license_detections = getattr (resource , 'license_detections' , []) or []
668+ license_clues = getattr (resource , 'license_clues' , []) or []
669+
670+ if license_detections :
671+ license_detection_objects = detections_from_license_detection_mappings (
672+ license_detection_mappings = license_detections ,
673+ file_path = resource .path ,
674+ )
675+ resource_license_detections .extend (license_detection_objects )
676+
677+ if include_license_clues and license_clues :
678+ license_matches = LicenseMatchFromResult .from_dicts (
679+ license_match_mappings = license_clues ,
680+ )
681+
682+ for group_of_matches in group_matches (license_matches = license_matches ):
683+ detection = LicenseDetection .from_matches (matches = group_of_matches )
684+ detection .file_region = detection .get_file_region (path = resource .path )
685+ resource_license_detections .append (detection )
686+
687+ all_license_detections .extend (
688+ list (process_detections (detections = resource_license_detections ))
689+ )
690+
691+ if TRACE :
692+ logger_debug (
693+ f'before process_detections licenses:' ,
694+ f'resource_license_detections: { resource_license_detections } \n ' ,
695+ f'all_license_detections: { all_license_detections } ' ,
696+ )
697+
698+ if has_packages :
699+ package_data = getattr (resource , 'package_data' , []) or []
700+
701+ package_license_detection_mappings = []
702+ for package in package_data :
703+
704+ if package ["license_detections" ]:
705+ package_license_detection_mappings .extend (package ["license_detections" ])
706+
707+ if package ["other_license_detections" ]:
708+ package_license_detection_mappings .extend (package ["other_license_detections" ])
709+
710+ if package_license_detection_mappings :
711+ package_license_detection_objects = detections_from_license_detection_mappings (
712+ license_detection_mappings = package_license_detection_mappings ,
713+ file_path = resource .path ,
714+ )
715+
716+ all_license_detections .extend (package_license_detection_objects )
717+
718+ return all_license_detections
719+
616720
617721@attr .s
618722class UniqueDetection :
@@ -624,7 +728,7 @@ class UniqueDetection:
624728 detection_count = attr .ib (default = None )
625729 matches = attr .ib (default = attr .Factory (list ))
626730 detection_log = attr .ib (default = attr .Factory (list ))
627- files = attr .ib (factory = list )
731+ file_regions = attr .ib (factory = list )
628732
629733 @classmethod
630734 def get_unique_detections (cls , license_detections ):
@@ -640,17 +744,20 @@ def get_unique_detections(cls, license_detections):
640744 detection .file_region
641745 for detection in all_detections
642746 ]
643-
644747 detection = next (iter (all_detections ))
645- detection_mapping = detection .to_dict ()
748+ detection_log = []
749+ if hasattr (detection , "detection_log" ):
750+ if detection .detection_log :
751+ detection_log .extend (detection .detection_log )
752+
646753 unique_license_detections .append (
647754 cls (
648- identifier = detection_mapping [ " identifier" ] ,
649- license_expression = detection_mapping [ " license_expression" ] ,
650- detection_log = detection_mapping . get ( " detection_log" , []) or [],
651- matches = detection_mapping [ " matches" ] ,
755+ identifier = detection . identifier ,
756+ license_expression = detection . license_expression ,
757+ detection_log = detection_log or [],
758+ matches = detection . matches ,
652759 detection_count = len (file_regions ),
653- files = file_regions ,
760+ file_regions = file_regions ,
654761 )
655762 )
656763
@@ -660,7 +767,7 @@ def to_dict(self, license_diagnostics):
660767
661768 def dict_fields (attr , value ):
662769
663- if attr .name == 'files ' :
770+ if attr .name == 'file_regions ' :
664771 return False
665772
666773 if attr .name == 'matches' :
@@ -673,6 +780,15 @@ def dict_fields(attr, value):
673780
674781 return attr .asdict (self , filter = dict_fields )
675782
783+ def get_license_detection_object (self ):
784+ return LicenseDetection (
785+ license_expression = self .license_expression ,
786+ detection_log = self .detection_log ,
787+ matches = self .matches ,
788+ identifier = self .identifier ,
789+ file_region = None ,
790+ )
791+
676792
677793def get_detections_by_id (license_detections ):
678794 """
@@ -1215,6 +1331,35 @@ def get_license_keys_from_detections(license_detections, licensing=Licensing()):
12151331 return list (license_keys )
12161332
12171333
1334+ def get_ambiguous_license_detections_by_type (unique_license_detections ):
1335+ """
1336+ Return a list of ambiguous unique license detections which needs review
1337+ and would be todo items for the reviewer from a list of
1338+ `unique_license_detections`.
1339+ """
1340+
1341+ ambi_license_detections = {}
1342+
1343+ for detection in unique_license_detections :
1344+ if is_undetected_license_matches (license_matches = detection .matches ):
1345+ ambi_license_detections [DetectionCategory .UNDETECTED_LICENSE .value ] = detection
1346+
1347+ elif "unknown" in detection .license_expression :
1348+ if has_unknown_matches (license_matches = detection .matches ):
1349+ ambi_license_detections [DetectionCategory .UNKNOWN_MATCH .value ] = detection
1350+
1351+ elif is_match_coverage_less_than_threshold (
1352+ license_matches = detection .matches ,
1353+ threshold = IMPERFECT_MATCH_COVERAGE_THR ,
1354+ ):
1355+ ambi_license_detections [DetectionCategory .IMPERFECT_COVERAGE .value ] = detection
1356+
1357+ elif has_extra_words (license_matches = detection .matches ):
1358+ ambi_license_detections [DetectionCategory .EXTRA_WORDS .value ] = detection
1359+
1360+ return ambi_license_detections
1361+
1362+
12181363def analyze_detection (license_matches , package_license = False ):
12191364 """
12201365 Analyse a list of LicenseMatch objects, and determine if the license detection
0 commit comments