Skip to content

Commit

Permalink
Implement support for SPDX-FileContributor
Browse files Browse the repository at this point in the history
This patch adds a new parameter that allows to list one or more contributors
which will generate the corresponding list of fields in the header.

It refactors SpdxInfo into a dataclass so that it can be used with an optional
list of contributors in a simple fashion.
  • Loading branch information
sgaist committed Feb 7, 2023
1 parent 448dd34 commit c9f7aea
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 45 deletions.
23 changes: 16 additions & 7 deletions src/reuse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
import os
import re
from dataclasses import dataclass, field
from typing import NamedTuple, Set

from boolean.boolean import Expression
Expand Down Expand Up @@ -72,13 +73,21 @@
# Combine SPDX patterns into file patterns to ease default ignore usage
_IGNORE_FILE_PATTERNS.extend(_IGNORE_SPDX_PATTERNS)

#: Simple structure for holding SPDX information.
#:
#: The two iterables MUST be sets.
SpdxInfo = NamedTuple(
"SpdxInfo",
[("spdx_expressions", Set[Expression]), ("copyright_lines", Set[str])],
)

@dataclass
class SpdxInfo:
"""Simple class holding SPDX information"""

spdx_expressions: Set[Expression]
copyright_lines: Set[str]
contributor_lines: Set[str] = field(default_factory=set)

def __bool__(self):
return bool(
self.spdx_expressions
or self.copyright_lines
or self.contributor_lines
)


class ReuseException(Exception):
Expand Down
3 changes: 3 additions & 0 deletions src/reuse/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ def parser() -> argparse.ArgumentParser:
" copyright holders and licenses to add to the headers of the"
" given files.\n"
"\n"
"By using --contributor, you can specify people or entity that"
"contributed but are not copyright holder of the given files."
"\n"
"The first comment is replaced with a new header containing"
" the new copyright and licensing information and its former"
" copyright and licensing. If you want to keep the first"
Expand Down
2 changes: 1 addition & 1 deletion src/reuse/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def filter_ignore_block(text: str) -> str:
def contains_spdx_info(text: str) -> bool:
"""The text contains SPDX info."""
try:
return any(extract_spdx_info(text))
return bool(extract_spdx_info(text))
except (ExpressionError, ParseError):
return False

Expand Down
13 changes: 12 additions & 1 deletion src/reuse/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def _create_new_header(

rendered = template.render(
copyright_lines=sorted(spdx_info.copyright_lines),
contributor_lines=sorted(spdx_info.contributor_lines),
spdx_expressions=sorted(map(str, spdx_info.spdx_expressions)),
).strip("\n")

Expand Down Expand Up @@ -176,6 +177,7 @@ def create_header(
spdx_info = SpdxInfo(
spdx_info.spdx_expressions.union(existing_spdx.spdx_expressions),
spdx_copyrights,
spdx_info.contributor_lines.union(existing_spdx.contributor_lines),
)

new_header += _create_new_header(
Expand Down Expand Up @@ -575,6 +577,12 @@ def add_arguments(parser) -> None:
type=spdx_identifier,
help=_("SPDX Identifier, repeatable"),
)
parser.add_argument(
"--contributor",
action="append",
type=str,
help=_("file contributor, repeatable"),
)
parser.add_argument(
"--year",
"-y",
Expand Down Expand Up @@ -768,8 +776,11 @@ def run(args, project: Project, out=sys.stdout) -> int:
if args.copyright is not None
else set()
)
contributors = (
set(args.contributor) if args.contributor is not None else set()
)

spdx_info = SpdxInfo(expressions, copyright_lines)
spdx_info = SpdxInfo(expressions, copyright_lines, contributors)

result = 0
for path in paths:
Expand Down
2 changes: 1 addition & 1 deletion src/reuse/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def spdx_info_of(self, path: PathLike) -> SpdxInfo:
dep5_result = _copyright_from_dep5(
self.relative_from_root(path), self._copyright
)
if any(dep5_result):
if bool(dep5_result):
_LOGGER.info(
_("'{path}' covered by .reuse/dep5").format(path=path)
)
Expand Down
6 changes: 6 additions & 0 deletions src/reuse/templates/default_template.jinja2
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{% for copyright_line in copyright_lines %}
{{ copyright_line }}
{% endfor %}
{% for contributor_line in contributor_lines %}
{% if loop.first %}

{% endif %}
SPDX-FileContributor: {{ contributor_line }}
{% endfor %}

{% for expression in spdx_expressions %}
SPDX-License-Identifier: {{ expression }}
Expand Down
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,4 +374,13 @@ def mock_date_today(monkeypatch):
monkeypatch.setattr(datetime, "date", date)


@pytest.fixture(
params=[[], ["John Doe"], ["John Doe", "Alice Doe"]],
ids=["None", "John", "John and Alice"],
)
def contributors(request):
"""Provide contributors for SPDX-FileContributor field generation"""
yield request.param


# REUSE-IgnoreEnd
18 changes: 18 additions & 0 deletions tests/test_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def test_create_header_simple():
assert create_header(spdx_info).strip() == expected


def test_create_header_simple_with_contributor():
"""Create a super simple header."""
spdx_info = SpdxInfo(
{"GPL-3.0-or-later"}, {"SPDX-FileCopyrightText: Jane Doe"}, {"John Doe"}
)
expected = cleandoc(
"""
# SPDX-FileCopyrightText: Jane Doe
#
# SPDX-FileContributor: John Doe
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""
)

assert create_header(spdx_info).strip() == expected


def test_create_header_template_simple(template_simple):
"""Create a header with a simple template."""
spdx_info = SpdxInfo(
Expand Down
82 changes: 48 additions & 34 deletions tests/test_main_annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,59 +21,73 @@
# REUSE-IgnoreStart

# TODO: Replace this test with a monkeypatched test
def test_annotate_simple(fake_repository, stringio, mock_date_today):
def test_annotate_simple(
fake_repository, stringio, mock_date_today, contributors
):
"""Add a header to a file that does not have one."""
simple_file = fake_repository / "foo.py"
simple_file.write_text("pass")
expected = cleandoc(
"""
# SPDX-FileCopyrightText: 2018 Jane Doe
#
# SPDX-License-Identifier: GPL-3.0-or-later
content = ["# SPDX-FileCopyrightText: 2018 Jane Doe", "#"]

pass
"""
)
if contributors:
for contributor in sorted(contributors):
content.append(f"# SPDX-FileContributor: {contributor}")
content.append("#")

content += ["# SPDX-License-Identifier: GPL-3.0-or-later", "", "pass"]

expected = cleandoc("\n".join(content))

args = [
"annotate",
"--license",
"GPL-3.0-or-later",
"--copyright",
"Jane Doe",
]
for contributor in contributors:
args += ["--contributor", contributor]
args += ["foo.py"]

result = main(
[
"annotate",
"--license",
"GPL-3.0-or-later",
"--copyright",
"Jane Doe",
"foo.py",
],
args,
out=stringio,
)

assert result == 0
assert simple_file.read_text() == expected


def test_addheader_simple(fake_repository, stringio, mock_date_today):
def test_addheader_simple(
fake_repository, stringio, mock_date_today, contributors
):
"""Add a header to a file that does not have one."""
simple_file = fake_repository / "foo.py"
simple_file.write_text("pass")
expected = cleandoc(
"""
# SPDX-FileCopyrightText: 2018 Jane Doe
#
# SPDX-License-Identifier: GPL-3.0-or-later
content = ["# SPDX-FileCopyrightText: 2018 Jane Doe", "#"]

pass
"""
)
if contributors:
for contributor in sorted(contributors):
content.append(f"# SPDX-FileContributor: {contributor}")
content.append("#")

content += ["# SPDX-License-Identifier: GPL-3.0-or-later", "", "pass"]

expected = cleandoc("\n".join(content))

args = [
"addheader",
"--license",
"GPL-3.0-or-later",
"--copyright",
"Jane Doe",
]
for contributor in contributors:
args += ["--contributor", contributor]
args += ["foo.py"]

result = main(
[
"addheader",
"--license",
"GPL-3.0-or-later",
"--copyright",
"Jane Doe",
"foo.py",
],
args,
out=stringio,
)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def test_spdx_info_of_unlicensed_file(fake_repository):
"""
(fake_repository / "foo.py").write_text("foo")
project = Project(fake_repository)
assert not any(project.spdx_info_of("foo.py"))
assert not bool(project.spdx_info_of("foo.py"))


def test_spdx_info_of_only_copyright(fake_repository):
Expand Down

0 comments on commit c9f7aea

Please sign in to comment.