Skip to content

Commit 6090c9b

Browse files
authored
Merge pull request #1098 from carmenbianca/fix-plus
Fix downloading `SPDX-IDENTIFIER+`
2 parents eedf5d5 + ebb3bac commit 6090c9b

File tree

5 files changed

+117
-16
lines changed

5 files changed

+117
-16
lines changed

changelog.d/fixed/strip-plus.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- When running `reuse download SPDX-IDENTIFIER+`, download `SPDX-IDENTIFIER`
2+
instead. This also works for `reuse download --all`. (#1098)

src/reuse/_util.py

+26
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,32 @@ def _determine_license_suffix_path(path: StrPath) -> Path:
109109
return Path(f"{path}.license")
110110

111111

112+
def _strip_plus_from_identifier(spdx_identifier: str) -> str:
113+
"""Strip final plus from identifier.
114+
115+
>>> _strip_plus_from_identifier("EUPL-1.2+")
116+
'EUPL-1.2'
117+
>>> _strip_plus_from_identifier("EUPL-1.2")
118+
'EUPL-1.2'
119+
"""
120+
if spdx_identifier.endswith("+"):
121+
return spdx_identifier[:-1]
122+
return spdx_identifier
123+
124+
125+
def _add_plus_to_identifier(spdx_identifier: str) -> str:
126+
"""Add final plus to identifier.
127+
128+
>>> _add_plus_to_identifier("EUPL-1.2")
129+
'EUPL-1.2+'
130+
>>> _add_plus_to_identifier("EUPL-1.2+")
131+
'EUPL-1.2+'
132+
"""
133+
if spdx_identifier.endswith("+"):
134+
return spdx_identifier
135+
return f"{spdx_identifier}+"
136+
137+
112138
def relative_from_root(path: StrPath, root: StrPath) -> Path:
113139
"""A helper function to get *path* relative to *root*."""
114140
path = Path(path)

src/reuse/cli/download.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import click
1616

1717
from .._licenses import ALL_NON_DEPRECATED_MAP
18+
from .._util import _strip_plus_from_identifier
1819
from ..download import _path_to_license_file, put_license_in_file
1920
from ..i18n import _
2021
from ..report import ProjectReport
@@ -138,7 +139,7 @@ def _successfully_downloaded(destination: StrPath) -> None:
138139
),
139140
)
140141
@click.argument(
141-
"license_",
142+
"licenses",
142143
# TRANSLATORS: You may translate this. Please preserve capital letters.
143144
metavar=_("LICENSE"),
144145
type=str,
@@ -147,22 +148,20 @@ def _successfully_downloaded(destination: StrPath) -> None:
147148
@click.pass_obj
148149
def download(
149150
obj: ClickObj,
150-
license_: Collection[str],
151+
licenses: Collection[str],
151152
all_: bool,
152153
output: Optional[Path],
153154
source: Optional[Path],
154155
) -> None:
155156
# pylint: disable=missing-function-docstring
156-
if all_ and license_:
157+
if all_ and licenses:
157158
raise click.UsageError(
158159
_(
159160
"The 'LICENSE' argument and '--all' option are mutually"
160161
" exclusive."
161162
)
162163
)
163164

164-
licenses: Collection[str] = license_ # type: ignore
165-
166165
if all_:
167166
# TODO: This is fairly inefficient, but gets the job done.
168167
report = ProjectReport.generate(obj.project, do_checksum=False)
@@ -173,6 +172,7 @@ def download(
173172
_("Cannot use '--output' with more than one license.")
174173
)
175174

175+
licenses = {_strip_plus_from_identifier(lic) for lic in licenses}
176176
return_code = 0
177177
for lic in licenses:
178178
destination: Path = output # type: ignore

src/reuse/report.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
from uuid import uuid4
3333

3434
from . import _LICENSING, __REUSE_version__, __version__
35-
from ._util import _checksum
35+
from ._util import (
36+
_add_plus_to_identifier,
37+
_checksum,
38+
_strip_plus_from_identifier,
39+
)
3640
from .extract import _LICENSEREF_PATTERN
3741
from .global_licensing import ReuseDep5
3842
from .i18n import _
@@ -436,15 +440,13 @@ def unused_licenses(self) -> set[str]:
436440
if self._unused_licenses is not None:
437441
return self._unused_licenses
438442

439-
# First collect licenses that are suspected to be unused.
440-
suspected_unused_licenses = {
441-
lic for lic in self.licenses if lic not in self.used_licenses
442-
}
443-
# Remove false positives.
444443
self._unused_licenses = {
445444
lic
446-
for lic in suspected_unused_licenses
447-
if f"{lic}+" not in self.used_licenses
445+
for lic in self.licenses
446+
if not any(
447+
identifier in self.used_licenses
448+
for identifier in set((lic, _add_plus_to_identifier(lic)))
449+
)
448450
}
449451
return self._unused_licenses
450452

@@ -750,8 +752,12 @@ def generate(
750752
# A license expression akin to Apache-1.0+ should register
751753
# correctly if LICENSES/Apache-1.0.txt exists.
752754
identifiers = {identifier}
753-
if identifier.endswith("+"):
754-
identifiers.add(identifier[:-1])
755+
if (
756+
plus_identifier := _strip_plus_from_identifier(
757+
identifier
758+
)
759+
) != identifier:
760+
identifiers.add(plus_identifier)
755761
# Bad license
756762
if not identifiers.intersection(project.license_map):
757763
report.bad_licenses.add(identifier)

tests/test_cli_download.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
"""Tests for download."""
66

7-
# pylint: disable=redefined-outer-name,unused-argument
7+
# pylint: disable=redefined-outer-name,unused-argument,unspecified-encoding
88

99
import errno
1010
import os
11+
import shutil
1112
from pathlib import Path
1213
from unittest.mock import create_autospec
1314
from urllib.error import URLError
@@ -18,6 +19,8 @@
1819
from reuse.cli import download
1920
from reuse.cli.main import main
2021

22+
# REUSE-IgnoreStart
23+
2124

2225
@pytest.fixture()
2326
def mock_put_license_in_file(monkeypatch):
@@ -39,6 +42,67 @@ def test_simple(self, empty_directory, mock_put_license_in_file):
3942
"0BSD", Path(os.path.realpath("LICENSES/0BSD.txt")), source=None
4043
)
4144

45+
def test_strip_plus(self, empty_directory, mock_put_license_in_file):
46+
"""If downloading LIC+, download LIC instead."""
47+
result = CliRunner().invoke(main, ["download", "EUPL-1.2+"])
48+
49+
assert result.exit_code == 0
50+
mock_put_license_in_file.assert_called_with(
51+
"EUPL-1.2",
52+
Path(os.path.realpath("LICENSES/EUPL-1.2.txt")),
53+
source=None,
54+
)
55+
56+
def test_all(self, fake_repository, mock_put_license_in_file):
57+
"""--all downloads all detected licenses."""
58+
shutil.rmtree("LICENSES")
59+
result = CliRunner().invoke(main, ["download", "--all"])
60+
61+
assert result.exit_code == 0
62+
for lic in [
63+
"GPL-3.0-or-later",
64+
"LicenseRef-custom",
65+
"Autoconf-exception-3.0",
66+
"Apache-2.0",
67+
"CC0-1.0",
68+
]:
69+
mock_put_license_in_file.assert_any_call(
70+
lic, Path(os.path.realpath(f"LICENSES/{lic}.txt")), source=None
71+
)
72+
73+
def test_all_with_plus(self, fake_repository, mock_put_license_in_file):
74+
"""--all downloads EUPL-1.2 if EUPL-1.2+ is detected."""
75+
Path("foo.py").write_text("# SPDX-License-Identifier: EUPL-1.2+")
76+
result = CliRunner().invoke(main, ["download", "--all"])
77+
78+
assert result.exit_code == 0
79+
mock_put_license_in_file.assert_called_once_with(
80+
"EUPL-1.2",
81+
Path(os.path.realpath("LICENSES/EUPL-1.2.txt")),
82+
source=None,
83+
)
84+
85+
def test_all_with_plus_and_non_plus(
86+
self, fake_repository, mock_put_license_in_file
87+
):
88+
"""If both EUPL-1.2 and EUPL-1.2+ is detected, download EUPL-1.2 only
89+
once.
90+
"""
91+
Path("foo.py").write_text(
92+
"""
93+
# SPDX-License-Identifier: EUPL-1.2+
94+
# SPDX-License-Identifier: EUPL-1.2
95+
"""
96+
)
97+
result = CliRunner().invoke(main, ["download", "--all"])
98+
99+
assert result.exit_code == 0
100+
mock_put_license_in_file.assert_called_once_with(
101+
"EUPL-1.2",
102+
Path(os.path.realpath("LICENSES/EUPL-1.2.txt")),
103+
source=None,
104+
)
105+
42106
def test_all_and_license_mutually_exclusive(self, empty_directory):
43107
"""--all and license args are mutually exclusive."""
44108
result = CliRunner().invoke(main, ["download", "--all", "0BSD"])
@@ -223,3 +287,6 @@ def test_prefix(self):
223287
result = download._similar_spdx_identifiers("CC0")
224288

225289
assert "CC0-1.0" in result
290+
291+
292+
# REUSE-IgnoreEnd

0 commit comments

Comments
 (0)