Skip to content

Commit

Permalink
Merge pull request #697 from sgaist/677_support_licenseref_init
Browse files Browse the repository at this point in the history
Add support for using LicenseRef- when initialising
  • Loading branch information
carmenbianca committed Oct 24, 2023
2 parents f949014 + bb368b6 commit 132762d
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ CLI command and its behaviour. There are no guarantees of stability for the

### Added

- Implement handling LicenseRef in `download` and `init`. (#697)
- Declared support for Python 3.12. (#846)
- More file types are recognised:
- Julia (`.jl`) (#815)
Expand Down
2 changes: 2 additions & 0 deletions src/reuse/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
"symbol": "©",
}

_LICENSEREF_PATTERN = re.compile("LicenseRef-[a-zA-Z0-9-.]+$")

# Amount of bytes that we assume will be big enough to contain the entire
# comment header (including SPDX tags), so that we don't need to read the
# entire file.
Expand Down
59 changes: 51 additions & 8 deletions src/reuse/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@

import errno
import logging
import os
import shutil
import sys
import urllib.request
from argparse import ArgumentParser, Namespace
from gettext import gettext as _
from pathlib import Path
from typing import IO
from typing import IO, Optional
from urllib.error import URLError
from urllib.parse import urljoin

from ._licenses import ALL_NON_DEPRECATED_MAP
from ._util import (
_LICENSEREF_PATTERN,
PathType,
StrPath,
find_licenses_directory,
Expand Down Expand Up @@ -61,30 +64,53 @@ def _path_to_license_file(spdx_identifier: str, root: StrPath) -> Path:
return licenses_path / "".join((spdx_identifier, ".txt"))


def put_license_in_file(spdx_identifier: str, destination: StrPath) -> None:
def put_license_in_file(
spdx_identifier: str,
destination: StrPath,
source: Optional[StrPath] = None,
) -> None:
"""Download a license and put it in the destination file.
This function exists solely for convenience.
Args:
spdx_identifier: SPDX License Identifier of the license.
destination: Where to put the license.
source: Path to file or directory containing the text for LicenseRef
licenses.
Raises:
URLError: if the license could not be downloaded.
FileExistsError: if the license file already exists.
FileNotFoundError: if the source could not be found in the directory.
"""
header = ""
destination = Path(destination)
destination.parent.mkdir(exist_ok=True)

if destination.exists():
raise FileExistsError(errno.EEXIST, "File exists", str(destination))
raise FileExistsError(
errno.EEXIST, os.strerror(errno.EEXIST), str(destination)
)

text = download_license(spdx_identifier)
with destination.open("w", encoding="utf-8") as fp:
fp.write(header)
fp.write(text)
# LicenseRef- license; don't download anything.
if _LICENSEREF_PATTERN.match(spdx_identifier):
if source:
source = Path(source)
if source.is_dir():
source = source / f"{spdx_identifier}.txt"
if not source.exists():
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), str(source)
)
shutil.copyfile(source, destination)
else:
destination.touch()
else:
text = download_license(spdx_identifier)
with destination.open("w", encoding="utf-8") as fp:
fp.write(header)
fp.write(text)


def add_arguments(parser: ArgumentParser) -> None:
Expand All @@ -103,6 +129,15 @@ def add_arguments(parser: ArgumentParser) -> None:
parser.add_argument(
"--output", "-o", dest="file", action="store", type=PathType("w")
)
parser.add_argument(
"--source",
action="store",
type=PathType("r"),
help=_(
"source from which to copy custom LicenseRef- licenses, either"
" a directory that contains the file or the file itself"
),
)


def run(args: Namespace, project: Project, out: IO[str] = sys.stdout) -> int:
Expand All @@ -116,6 +151,9 @@ def _already_exists(path: StrPath) -> None:
)
out.write("\n")

def _not_found(path: StrPath) -> None:
out.write(_("Error: {path} does not exist.").format(path=path))

def _could_not_download(identifier: str) -> None:
out.write(_("Error: Failed to download license."))
out.write(" ")
Expand Down Expand Up @@ -156,13 +194,18 @@ def _successfully_downloaded(destination: StrPath) -> None:
else:
destination = _path_to_license_file(lic, project.root)
try:
put_license_in_file(lic, destination=destination)
put_license_in_file(
lic, destination=destination, source=args.source
)
except URLError:
_could_not_download(lic)
return_code = 1
except FileExistsError as err:
_already_exists(err.filename)
return_code = 1
except FileNotFoundError as err:
_not_found(err.filename)
return_code = 1
else:
_successfully_downloaded(destination)
return return_code
22 changes: 19 additions & 3 deletions src/reuse/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

"""Functions for REUSE-ifying a project."""

import re
import sys
from argparse import ArgumentParser, Namespace
from gettext import gettext as _
Expand All @@ -13,7 +14,11 @@
from urllib.error import URLError

from ._licenses import ALL_NON_DEPRECATED_MAP
from ._util import PathType, print_incorrect_spdx_identifier
from ._util import (
_LICENSEREF_PATTERN,
PathType,
print_incorrect_spdx_identifier,
)
from .download import _path_to_license_file, put_license_in_file
from .project import Project
from .vcs import find_root
Expand Down Expand Up @@ -44,7 +49,9 @@ def prompt_licenses(out: IO[str] = sys.stdout) -> List[str]:
out.write("\n")
if not result:
return licenses
if result not in ALL_NON_DEPRECATED_MAP:
if result not in ALL_NON_DEPRECATED_MAP and not re.match(
_LICENSEREF_PATTERN, result
):
print_incorrect_spdx_identifier(result, out=out)
out.write("\n\n")
else:
Expand Down Expand Up @@ -116,8 +123,9 @@ def run(

for lic in licenses:
destination = _path_to_license_file(lic, root=root)

try:
out.write(_("Downloading {}").format(lic))
out.write(_("Retrieving {}").format(lic))
out.write("\n")
put_license_in_file(lic, destination=destination)
# TODO: exceptions
Expand All @@ -127,6 +135,14 @@ def run(
except URLError:
out.write(_("Could not download {}").format(lic))
out.write("\n")
except FileNotFoundError as err:
out.write(
_(
"Error: Could not copy {path}, "
"please add {lic}.txt manually in the LICENCES/ directory."
).format(path=err.filename, lic=lic)
)
out.write("\n")

out.write("\n")

Expand Down
5 changes: 3 additions & 2 deletions src/reuse/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from ._licenses import EXCEPTION_MAP, LICENSE_MAP
from ._util import (
_HEADER_BYTES,
_LICENSEREF_PATTERN,
GIT_EXE,
HG_EXE,
StrPath,
Expand Down Expand Up @@ -304,7 +305,7 @@ def _identifier_of_license(self, path: Path) -> str:
raise IdentifierNotFound(f"{path} has no file extension")
if path.stem in self.license_map:
return path.stem
if path.stem.startswith("LicenseRef-"):
if _LICENSEREF_PATTERN.match(path.stem):
return path.stem

raise IdentifierNotFound(
Expand Down Expand Up @@ -396,7 +397,7 @@ def _licenses(self) -> Dict[str, Path]:
# Add the identifiers
license_files[identifier] = path
if (
identifier.startswith("LicenseRef-")
_LICENSEREF_PATTERN.match(identifier)
and "Unknown" not in identifier
):
self.license_map[identifier] = {
Expand Down
4 changes: 2 additions & 2 deletions src/reuse/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from uuid import uuid4

from . import __REUSE_version__, __version__
from ._util import _LICENSING, StrPath, _checksum
from ._util import _LICENSEREF_PATTERN, _LICENSING, StrPath, _checksum
from .project import Project, ReuseInfo

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -226,7 +226,7 @@ def bill_of_materials(

# Licenses
for lic, path in sorted(self.licenses.items()):
if lic.startswith("LicenseRef-"):
if _LICENSEREF_PATTERN.match(lic):
out.write("\n")
out.write(f"LicenseID: {lic}\n")
out.write("LicenseName: NOASSERTION\n")
Expand Down
62 changes: 62 additions & 0 deletions tests/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,65 @@ def test_put_empty_dir(empty_directory, monkeypatch):

assert (empty_directory / "LICENSES").exists()
assert (empty_directory / "LICENSES/0BSD.txt").read_text() == "hello\n"


def test_put_custom_without_source(fake_repository):
"""When 'downloading' a LicenseRef license without source, create an empty
file.
"""
put_license_in_file("LicenseRef-hello", "LICENSES/LicenseRef-hello.txt")

assert (fake_repository / "LICENSES/LicenseRef-hello.txt").exists()
assert (fake_repository / "LICENSES/LicenseRef-hello.txt").read_text() == ""


def test_put_custom_with_source(fake_repository):
"""When 'downloading' a LicenseRef license with source file, copy the source
text.
"""
(fake_repository / "foo.txt").write_text("foo")

put_license_in_file(
"LicenseRef-hello",
"LICENSES/LicenseRef-hello.txt",
source=fake_repository / "foo.txt",
)

assert (fake_repository / "LICENSES/LicenseRef-hello.txt").exists()
assert (
fake_repository / "LICENSES/LicenseRef-hello.txt"
).read_text() == "foo"


def test_put_custom_with_source_dir(fake_repository):
"""When 'downloading' a LicenseRef license with source directory, copy the
source text from a matching file in the directory.
"""
(fake_repository / "lics").mkdir()
(fake_repository / "lics/LicenseRef-hello.txt").write_text("foo")

put_license_in_file(
"LicenseRef-hello",
"LICENSES/LicenseRef-hello.txt",
source=fake_repository / "lics",
)

assert (fake_repository / "LICENSES/LicenseRef-hello.txt").exists()
assert (
fake_repository / "LICENSES/LicenseRef-hello.txt"
).read_text() == "foo"


def test_put_custom_with_false_source_dir(fake_repository):
"""When 'downloading' a LicenseRef license with source directory, but the
source directory does not contain the license, expect a FileNotFoundError.
"""
(fake_repository / "lics").mkdir()

with pytest.raises(FileNotFoundError) as exc_info:
put_license_in_file(
"LicenseRef-hello",
"LICENSES/LicenseRef-hello.txt",
source=fake_repository / "lics",
)
assert exc_info.value.filename.endswith("lics/LicenseRef-hello.txt")
49 changes: 47 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def test_download(fake_repository, stringio, mock_put_license_in_file):

assert result == 0
mock_put_license_in_file.assert_called_with(
"0BSD", Path("LICENSES/0BSD.txt").resolve()
"0BSD", Path("LICENSES/0BSD.txt").resolve(), source=None
)


Expand Down Expand Up @@ -434,7 +434,9 @@ def test_download_custom_output(
result = main(["download", "-o", "foo", "0BSD"], out=stringio)

assert result == 0
mock_put_license_in_file.assert_called_with("0BSD", destination=Path("foo"))
mock_put_license_in_file.assert_called_with(
"0BSD", destination=Path("foo"), source=None
)


def test_download_custom_output_too_many(
Expand All @@ -449,6 +451,49 @@ def test_download_custom_output_too_many(
)


def test_download_licenseref_no_source(empty_directory, stringio):
"""Downloading a LicenseRef license creates an empty file."""
main(["download", "LicenseRef-hello"], out=stringio)
assert (empty_directory / "LICENSES/LicenseRef-hello.txt").read_text() == ""


def test_download_licenseref_source_file(empty_directory, stringio):
"""Downloading a LicenseRef license with a source file copies that file's
contents.
"""
(empty_directory / "foo.txt").write_text("foo")
main(["download", "--source", "foo.txt", "LicenseRef-hello"], out=stringio)
assert (
empty_directory / "LICENSES/LicenseRef-hello.txt"
).read_text() == "foo"


def test_download_licenseref_source_dir(empty_directory, stringio):
"""Downloading a LicenseRef license with a source dir copies the text from
the corresponding file in the directory.
"""
(empty_directory / "lics").mkdir()
(empty_directory / "lics/LicenseRef-hello.txt").write_text("foo")

main(["download", "--source", "lics", "LicenseRef-hello"], out=stringio)
assert (
empty_directory / "LICENSES/LicenseRef-hello.txt"
).read_text() == "foo"


def test_download_licenseref_false_source_dir(empty_directory, stringio):
"""Downloading a LicenseRef license with a source that does not contain the
license results in an error.
"""
(empty_directory / "lics").mkdir()

result = main(
["download", "--source", "lics", "LicenseRef-hello"], out=stringio
)
assert result != 0
assert "lics/LicenseRef-hello.txt does not exist" in stringio.getvalue()


def test_supported_licenses(stringio):
"""Invoke the supported-licenses command and check whether the result
contains at least one license in the expected format.
Expand Down

0 comments on commit 132762d

Please sign in to comment.