Skip to content

Commit e8ea016

Browse files
committed
Support multiple licenses per project
1 parent d3f726d commit e8ea016

6 files changed

Lines changed: 67 additions & 20 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Release 0.11.0 (unreleased)
88
* Use CycloneDX schema version 1.6 (#542)
99
* Add security policy (#784)
1010
* Add provenance / release attestation to pypi package (#784)
11+
* Support multiple licenses per project (#788)
1112

1213
Release 0.10.0 (released 2025-03-12)
1314
====================================

dfetch/commands/report.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import argparse
77
import glob
88
import os
9-
10-
import infer_license
9+
from typing import List, Tuple
1110

1211
import dfetch.commands.command
1312
import dfetch.manifest.manifest
@@ -17,9 +16,12 @@
1716
from dfetch.project.metadata import Metadata
1817
from dfetch.project.vcs import VCS
1918
from dfetch.reporting import REPORTERS, ReportTypes
19+
from dfetch.util.license import guess_license_in_file
2020

2121
logger = get_logger(__name__)
2222

23+
LICENSE_PROBABILITY_THRESHOLD = 0.80
24+
2325

2426
class Report(dfetch.commands.command.Command):
2527
"""Generate reports containing information about the projects components.
@@ -66,37 +68,38 @@ def __call__(self, args: argparse.Namespace) -> None:
6668

6769
with dfetch.util.util.in_directory(os.path.dirname(path)):
6870
for project in manifest.selected_projects(args.projects):
69-
determined_license = self._determine_license(project)
71+
determined_licenses = self._determine_licenses(project)
7072
version = self._determine_version(project)
7173
reporter.add_project(
72-
project=project, license_name=determined_license, version=version
74+
project=project, license_names=determined_licenses, version=version
7375
)
7476

7577
if reporter.dump_to_file(args.outfile):
7678
logger.info(f"Generated {reporter.name} report: {args.outfile}")
7779

7880
@staticmethod
79-
def _determine_license(project: ProjectEntry) -> str:
81+
def _determine_licenses(project: ProjectEntry) -> List[Tuple[str, float]]:
8082
"""Try to determine license of fetched project."""
8183
if not os.path.exists(project.destination):
8284
logger.print_warning_line(
8385
project.name, "Never fetched, fetch it to get license info."
8486
)
85-
return ""
87+
return []
8688

89+
license_files = []
8790
with dfetch.util.util.in_directory(project.destination):
91+
8892
for license_file in filter(VCS.is_license_file, glob.glob("*")):
8993
logger.debug(f"Found license file {license_file} for {project.name}")
90-
guessed_license = infer_license.api.guess_file(license_file)
94+
guessed_license, probability = guess_license_in_file(license_file)
9195

9296
if guessed_license:
93-
return str(guessed_license.name)
94-
95-
logger.print_warning_line(
96-
project.name, f"Could not determine license in {license_file}"
97-
)
98-
99-
return ""
97+
license_files.append((str(guessed_license.name), probability))
98+
else:
99+
logger.print_warning_line(
100+
project.name, f"Could not determine license in {license_file}"
101+
)
102+
return license_files
100103

101104
@staticmethod
102105
def _determine_version(project: ProjectEntry) -> str:

dfetch/reporting/reporter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Abstract reporting interface."""
22

33
from abc import ABC, abstractmethod
4+
from typing import List, Tuple
45

56
from dfetch.manifest.project import ProjectEntry
67

@@ -12,7 +13,10 @@ class Reporter(ABC):
1213

1314
@abstractmethod
1415
def add_project(
15-
self, project: ProjectEntry, license_name: str, version: str
16+
self,
17+
project: ProjectEntry,
18+
license_names: List[Tuple[str, float]],
19+
version: str,
1620
) -> None:
1721
"""Add a project to the report."""
1822

dfetch/reporting/sbom_reporter.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
An fetched project generates an sbom
1616
"""
1717

18+
from typing import List, Tuple
19+
1820
from cyclonedx.builder.this import this_component as cdx_lib_component
1921
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
2022
from cyclonedx.model.bom import Bom
@@ -48,7 +50,10 @@ def __init__(self) -> None:
4850
self._bom.metadata.tools.components.add(cdx_lib_component())
4951

5052
def add_project(
51-
self, project: ProjectEntry, license_name: str, version: str
53+
self,
54+
project: ProjectEntry,
55+
license_names: List[Tuple[str, float]],
56+
version: str,
5257
) -> None:
5358
"""Add a project to the report."""
5459
purl = dfetch.util.purl.remote_url_to_purl(
@@ -89,8 +94,8 @@ def add_project(
8994
)
9095
)
9196

92-
if license_name:
93-
component.licenses.add(LicenseExpression(license_name))
97+
for name, _ in license_names:
98+
component.licenses.add(LicenseExpression(name))
9499
self._bom.components.add(component)
95100

96101
def dump_to_file(self, outfile: str) -> bool:

dfetch/reporting/stdout_reporter.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from the manifest or the metadata (``.dfetch_data.yaml``).
55
"""
66

7+
from typing import List, Tuple
8+
79
from dfetch.log import get_logger
810
from dfetch.manifest.project import ProjectEntry
911
from dfetch.project.metadata import Metadata
@@ -18,7 +20,10 @@ class StdoutReporter(Reporter):
1820
name = "stdout"
1921

2022
def add_project(
21-
self, project: ProjectEntry, license_name: str, version: str
23+
self,
24+
project: ProjectEntry,
25+
license_names: List[Tuple[str, float]],
26+
version: str,
2227
) -> None:
2328
"""Add a project to the report."""
2429
del version
@@ -32,7 +37,9 @@ def add_project(
3237
logger.print_info_field(" last fetch", str(metadata.last_fetch))
3338
logger.print_info_field(" revision", metadata.revision)
3439
logger.print_info_field(" patch", metadata.patch)
35-
logger.print_info_field(" license", license_name)
40+
logger.print_info_field(
41+
" licenses", ",".join(license for license, _ in license_names)
42+
)
3643

3744
except FileNotFoundError:
3845
logger.print_info_field(" last fetch", "never")

dfetch/util/license.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""*Dfetch* uses *Infer-License* to guess licenses from files."""
2+
3+
import os
4+
from typing import Optional, Tuple, Union
5+
6+
import infer_license
7+
from infer_license.types import License
8+
9+
LICENSE_PROBABILITY_THRESHOLD = 0.80
10+
11+
12+
def guess_license_in_file(
13+
filename: Union[str, "os.PathLike[str]"],
14+
) -> Tuple[Optional[License], float]:
15+
"""Guess license from file."""
16+
try:
17+
with open(filename, encoding="utf-8") as f:
18+
license_text = f.read()
19+
except UnicodeDecodeError:
20+
with open(filename, encoding="latin-1") as f:
21+
license_text = f.read()
22+
23+
probable_license = infer_license.api.probabilities(license_text)
24+
if probable_license and probable_license[0][1] > LICENSE_PROBABILITY_THRESHOLD:
25+
return probable_license[0]
26+
27+
return None, 0.0

0 commit comments

Comments
 (0)