Skip to content

Commit 116a3b1

Browse files
authored
Refactor inspect subcommand summary
* chore:SP-2777 Refactor on inspect license and component summary * Upgrades version to v1.26.1 * chore:Updates CHANGELOG.md file
1 parent 9f4b28e commit 116a3b1

File tree

8 files changed

+133
-131
lines changed

8 files changed

+133
-131
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [1.26.1] - 2025-06-23
13+
14+
### Added
15+
- Added component count to inspect license summary
16+
### Changed
17+
- Modified summaries for inspect subcommand
18+
1219
## [1.26.0] - 2025-06-20
1320
### Added
1421
- New `inspect license-summary` subcommand to generate license summaries from scan results
@@ -552,4 +559,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
552559
[1.25.0]: https://github.com/scanoss/scanoss.py/compare/v1.24.0...v1.25.0
553560
[1.25.1]: https://github.com/scanoss/scanoss.py/compare/v1.25.0...v1.25.1
554561
[1.25.2]: https://github.com/scanoss/scanoss.py/compare/v1.25.1...v1.25.2
555-
[1.26.0]: https://github.com/scanoss/scanoss.py/compare/v1.25.2...v1.26.0
562+
[1.26.0]: https://github.com/scanoss/scanoss.py/compare/v1.25.2...v1.26.0
563+
[1.26.1]: https://github.com/scanoss/scanoss.py/compare/v1.26.0...v1.26.1

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '1.26.0'
25+
__version__ = '1.26.1'

src/scanoss/inspection/component_summary.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ def _get_component_summary_from_components(self, scan_components: list)-> dict:
3535
:param components: List of all components
3636
:return: Dict with license summary information
3737
"""
38+
# A component is considered unique by its combination of PURL (Package URL) and license
39+
component_licenses = self._group_components_by_license(scan_components)
40+
total_components = len(component_licenses)
41+
# Get undeclared components
42+
undeclared_components = len([c for c in component_licenses if c['status'] == 'pending'])
43+
3844
components: list = []
39-
undeclared_components = 0
40-
total_components = 0
45+
total_undeclared_files = 0
46+
total_files_detected = 0
4147
for component in scan_components:
42-
total_components += component['count']
43-
undeclared_components += component['undeclared']
48+
total_files_detected += component['count']
49+
total_undeclared_files += component['undeclared']
4450
components.append({
4551
'purl': component['purl'],
4652
'version': component['version'],
@@ -50,10 +56,13 @@ def _get_component_summary_from_components(self, scan_components: list)-> dict:
5056
})
5157
## End for loop components
5258
return {
53-
'components': components,
54-
'total': total_components,
55-
'undeclared': undeclared_components,
56-
'declared': total_components - undeclared_components,
59+
"components": component_licenses,
60+
'totalComponents': total_components,
61+
'undeclaredComponents': undeclared_components,
62+
'declaredComponents': total_components - undeclared_components,
63+
'totalFilesDetected': total_files_detected,
64+
'totalFilesUndeclared': total_undeclared_files,
65+
'totalFilesDeclared': total_files_detected - total_undeclared_files,
5766
}
5867

5968
def _get_components(self):

src/scanoss/inspection/copyleft.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ def _json(self, components: list) -> Dict[str, Any]:
7878
:param components: List of components with copyleft licenses
7979
:return: Dictionary with formatted JSON details and summary
8080
"""
81+
# A component is considered unique by its combination of PURL (Package URL) and license
82+
component_licenses = self._group_components_by_license(components)
8183
details = {}
8284
if len(components) > 0:
8385
details = {'components': components}
8486
return {
8587
'details': f'{json.dumps(details, indent=2)}\n',
86-
'summary': f'{len(components)} component(s) with copyleft licenses were found.\n',
88+
'summary': f'{len(component_licenses)} component(s) with copyleft licenses were found.\n',
8789
}
8890

8991
def _markdown(self, components: list) -> Dict[str, Any]:
@@ -93,24 +95,24 @@ def _markdown(self, components: list) -> Dict[str, Any]:
9395
:param components: List of components with copyleft licenses
9496
:return: Dictionary with formatted Markdown details and summary
9597
"""
96-
headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']
98+
# A component is considered unique by its combination of PURL (Package URL) and license
99+
component_licenses = self._group_components_by_license(components)
100+
headers = ['Component', 'License', 'URL', 'Copyleft']
97101
centered_columns = [1, 4]
98102
rows: [[]] = []
99-
for component in components:
100-
for lic in component['licenses']:
103+
for comp_lic_item in component_licenses:
101104
row = [
102-
component['purl'],
103-
component['version'],
104-
lic['spdxid'],
105-
lic['url'],
106-
'YES' if lic['copyleft'] else 'NO',
105+
comp_lic_item['purl'],
106+
comp_lic_item['spdxid'],
107+
comp_lic_item['url'],
108+
'YES' if comp_lic_item['copyleft'] else 'NO',
107109
]
108110
rows.append(row)
109111
# End license loop
110112
# End component loop
111113
return {
112114
'details': f'### Copyleft licenses\n{self.generate_table(headers, rows, centered_columns)}\n',
113-
'summary': f'{len(components)} component(s) with copyleft licenses were found.\n',
115+
'summary': f'{len(component_licenses)} component(s) with copyleft licenses were found.\n',
114116
}
115117

116118
def _jira_markdown(self, components: list) -> Dict[str, Any]:
@@ -120,24 +122,24 @@ def _jira_markdown(self, components: list) -> Dict[str, Any]:
120122
:param components: List of components with copyleft licenses
121123
:return: Dictionary with formatted Markdown details and summary
122124
"""
123-
headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']
125+
# A component is considered unique by its combination of PURL (Package URL) and license
126+
component_licenses = self._group_components_by_license(components)
127+
headers = ['Component', 'License', 'URL', 'Copyleft']
124128
centered_columns = [1, 4]
125129
rows: [[]] = []
126-
for component in components:
127-
for lic in component['licenses']:
130+
for comp_lic_item in component_licenses:
128131
row = [
129-
component['purl'],
130-
component['version'],
131-
lic['spdxid'],
132-
lic['url'],
133-
'YES' if lic['copyleft'] else 'NO',
132+
comp_lic_item['purl'],
133+
comp_lic_item['spdxid'],
134+
comp_lic_item['url'],
135+
'YES' if comp_lic_item['copyleft'] else 'NO',
134136
]
135137
rows.append(row)
136138
# End license loop
137139
# End component loop
138140
return {
139141
'details': f'{self.generate_jira_table(headers, rows, centered_columns)}',
140-
'summary': f'{len(components)} component(s) with copyleft licenses were found.\n',
142+
'summary': f'{len(component_licenses)} component(s) with copyleft licenses were found.\n',
141143
}
142144

143145
def _filter_components_with_copyleft_licenses(self, components: list) -> list:
@@ -161,7 +163,6 @@ def _filter_components_with_copyleft_licenses(self, components: list) -> list:
161163
lic.pop('count', None) # None is default value if key doesn't exist
162164

163165
filtered_component['licenses'] = copyleft_licenses
164-
del filtered_component['status']
165166
filtered_components.append(filtered_component)
166167
# End component loop
167168
self.print_debug(f'Copyleft components: {filtered_components}')

src/scanoss/inspection/inspect_base.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,40 @@ def _get_licenses_order_by_source_priority(self,licenses_data):
372372
self.print_debug("No priority sources found, returning all licenses as list")
373373
return licenses_data
374374

375+
def _group_components_by_license(self,components):
376+
"""
377+
Groups components by their unique component-license pairs.
378+
379+
This method processes a list of components and creates unique entries for each
380+
component-license combination. If a component has multiple licenses, it will create
381+
separate entries for each license.
382+
383+
Args:
384+
components: A list of component dictionaries. Each component should have:
385+
- purl: Package URL identifying the component
386+
- licenses: List of license dictionaries, each containing:
387+
- spdxid: SPDX identifier for the license (optional)
388+
389+
Returns:
390+
list: A list of dictionaries, each containing:
391+
- purl: The component's package URL
392+
- license: The SPDX identifier of the license (or 'Unknown' if not provided)
393+
"""
394+
component_licenses: dict = {}
395+
for component in components:
396+
for lic in component['licenses']:
397+
spdxid = lic.get('spdxid', 'Unknown')
398+
if spdxid not in component_licenses:
399+
key = f'{component["purl"]}-{spdxid}'
400+
component_licenses[key] = {
401+
'purl': component['purl'],
402+
'spdxid': spdxid,
403+
'status': component['status'],
404+
'copyleft': lic['copyleft'],
405+
'url': lic['url'],
406+
}
407+
return list(component_licenses.values())
408+
375409

376410
#
377411
# End of PolicyCheck Class

src/scanoss/inspection/license_summary.py

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"""
2424

2525
import json
26-
from typing import Any, Dict
2726

2827
from .inspect_base import InspectBase
2928

@@ -73,63 +72,42 @@ def __init__( # noqa: PLR0913
7372
self.exclude = exclude
7473
self.explicit = explicit
7574

76-
def _validate_license(self, license_data: Dict[str, Any]) -> bool:
77-
"""
78-
Validate that a license has all required fields.
79-
80-
:param license_data: Dictionary containing license information
81-
:return: True if license is valid, False otherwise
82-
"""
83-
for field in self.REQUIRED_LICENSE_FIELDS:
84-
value = license_data.get(field)
85-
if value is None:
86-
self.print_debug(f'WARNING: {field} is empty in license: {license_data}')
87-
return False
88-
return True
89-
90-
def _append_license(self, licenses: dict, new_license) -> None:
91-
"""Add or update a license in the licenses' dictionary."""
92-
spdxid = new_license.get("spdxid")
93-
url = new_license.get("url")
94-
copyleft = new_license.get("copyleft")
95-
if spdxid not in licenses:
96-
licenses[spdxid] = {
97-
'spdxid': spdxid,
98-
'url': url,
99-
'copyleft':copyleft,
100-
'count': new_license.get("count"),
101-
}
102-
else:
103-
licenses[spdxid]['count'] += new_license.get("count")
104-
10575
def _get_licenses_summary_from_components(self, components: list)-> dict:
10676
"""
10777
Get a license summary from detected components.
10878
10979
:param components: List of all components
11080
:return: Dict with license summary information
11181
"""
82+
# A component is considered unique by its combination of PURL (Package URL) and license
83+
component_licenses = self._group_components_by_license(components)
84+
license_component_count = {}
85+
# Count license per component
86+
for lic in component_licenses:
87+
if lic['spdxid'] not in license_component_count:
88+
license_component_count[lic['spdxid']] = 1
89+
else:
90+
license_component_count[lic['spdxid']] += 1
11291
licenses:dict = {}
113-
licenses_with_copyleft = 0
114-
total_licenses = 0
115-
for component in components:
116-
component_licenses = component.get("licenses", [])
117-
for lic in component_licenses:
118-
if not self._validate_license(lic):
119-
continue
120-
copyleft = lic.get("copyleft")
121-
## Increment counters
122-
total_licenses += lic.get("count")
123-
if copyleft:
124-
licenses_with_copyleft += lic.get("count")
125-
## Add license
126-
self._append_license(licenses, lic)
92+
for comp_lic in component_licenses:
93+
spdxid = comp_lic.get("spdxid")
94+
url = comp_lic.get("url")
95+
copyleft = comp_lic.get("copyleft")
96+
if spdxid not in licenses:
97+
licenses[spdxid] = {
98+
'spdxid': spdxid,
99+
'url': url,
100+
'copyleft': copyleft,
101+
'componentCount': license_component_count.get(spdxid, 0), # Append component count to license
102+
}
127103
## End for loop licenses
128104
## End for loop components
105+
detected_licenses = list(licenses.values())
106+
licenses_with_copyleft = [lic for lic in detected_licenses if lic['copyleft']]
129107
return {
130-
'licenses': list(licenses.values()),
131-
'total': total_licenses,
132-
'copyleft': licenses_with_copyleft
108+
'licenses': detected_licenses,
109+
'detectedLicenses': len(detected_licenses), # Count unique licenses. SPDXID is considered unique
110+
'detectedLicensesWithCopyleft': len(licenses_with_copyleft),
133111
}
134112

135113

src/scanoss/inspection/undeclared_component.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ def _get_undeclared_component(self, components: list) -> list or None:
8080
for component in components:
8181
if component['status'] == 'pending':
8282
# Remove unused keys
83-
del component['status']
8483
del component['count']
8584
del component['declared']
8685
del component['undeclared']
@@ -177,7 +176,7 @@ def _markdown(self, components: list) -> Dict[str, Any]:
177176
# TODO look at using SpdxLite license name lookup method
178177
component_licenses = self._group_components_by_license(components)
179178
for component in component_licenses:
180-
rows.append([component.get('purl'), component.get('license')])
179+
rows.append([component.get('purl'), component.get('spdxid')])
181180
return {
182181
'details': f'### Undeclared components\n{self.generate_table(headers, rows)}\n',
183182
'summary': self._get_summary(component_licenses),
@@ -195,7 +194,7 @@ def _jira_markdown(self, components: list) -> Dict[str, Any]:
195194
# TODO look at using SpdxLite license name lookup method
196195
component_licenses = self._group_components_by_license(components)
197196
for component in component_licenses:
198-
rows.append([component.get('purl'), component.get('license')])
197+
rows.append([component.get('purl'), component.get('spdxid')])
199198
return {
200199
'details': f'{self.generate_jira_table(headers, rows)}',
201200
'summary': self._get_jira_summary(component_licenses),
@@ -265,36 +264,6 @@ def _get_components(self):
265264
# Convert to list and process licenses
266265
return self._convert_components_to_list(components)
267266

268-
def _group_components_by_license(self,components):
269-
"""
270-
Groups components by their unique component-license pairs.
271-
272-
This method processes a list of components and creates unique entries for each
273-
component-license combination. If a component has multiple licenses, it will create
274-
separate entries for each license.
275-
276-
Args:
277-
components: A list of component dictionaries. Each component should have:
278-
- purl: Package URL identifying the component
279-
- licenses: List of license dictionaries, each containing:
280-
- spdxid: SPDX identifier for the license (optional)
281-
282-
Returns:
283-
list: A list of dictionaries, each containing:
284-
- purl: The component's package URL
285-
- license: The SPDX identifier of the license (or 'Unknown' if not provided)
286-
"""
287-
component_licenses: dict = {}
288-
for component in components:
289-
for lic in component['licenses']:
290-
spdxid = lic.get('spdxid', 'Unknown')
291-
key = f'{component["purl"]}-{spdxid}'
292-
component_licenses[key] = {
293-
'purl': component['purl'],
294-
'license': spdxid,
295-
}
296-
return list(component_licenses.values())
297-
298267
def run(self):
299268
"""
300269
Run the undeclared component inspection process.

0 commit comments

Comments
 (0)