From 0a0459f947fa44e3f089e831ab4a0e767a362a37 Mon Sep 17 00:00:00 2001 From: Pietro Albini Date: Fri, 11 Nov 2022 09:41:30 +0100 Subject: [PATCH] add spdx --creator-person --creator-organization --- src/reuse/report.py | 22 ++++++++++++++++--- src/reuse/spdx.py | 36 +++++++++++++++++++++++++++++++- tests/test_main.py | 51 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/reuse/report.py b/src/reuse/report.py index 86c1dd5e6..0a4de3b23 100644 --- a/src/reuse/report.py +++ b/src/reuse/report.py @@ -103,7 +103,12 @@ def to_dict(self): "file_reports": [report.to_dict() for report in self.file_reports], } - def bill_of_materials(self) -> str: + def bill_of_materials( + self, + *, + creator_person: Optional[str] = None, + creator_organization: Optional[str] = None, + ) -> str: """Generate a bill of materials from the project. See https://spdx.org/specifications. @@ -124,8 +129,10 @@ def bill_of_materials(self) -> str: # Author # TODO: Fix Person and Organization - out.write("Creator: Person: Anonymous ()\n") - out.write("Creator: Organization: Anonymous ()\n") + out.write(f"Creator: Person: {self.format_creator(creator_person)}\n") + out.write( + f"Creator: Organization: {self.format_creator(creator_organization)}\n" + ) out.write(f"Creator: Tool: reuse-{__version__}\n") now = datetime.datetime.utcnow() @@ -176,6 +183,15 @@ def bill_of_materials(self) -> str: return out.getvalue() + def format_creator(self, creator): + if creator is None: + return "Anonymous ()" + elif "(" in creator and creator.endswith(")"): + # The creator field already contains an email address + return creator + else: + return creator + " ()" + @classmethod def generate( cls, diff --git a/src/reuse/spdx.py b/src/reuse/spdx.py index 5a3293e1f..2b1c209d3 100644 --- a/src/reuse/spdx.py +++ b/src/reuse/spdx.py @@ -31,10 +31,39 @@ def add_arguments(parser) -> None: "guarantee the field is accurate." ), ) + parser.add_argument( + "--creator-person", + metavar="NAME", + help=_("Name of the person signing off on the SPDX report"), + ) + parser.add_argument( + "--creator-organization", + metavar="NAME", + help=_("Name of the organization signing off on the SPDX report"), + ) def run(args, project: Project, out=sys.stdout) -> int: """Print the project's bill of materials.""" + # The SPDX spec mandates that a creator must be specified when a license + # conclusion is made, so here we enforce that. More context: + # + # https://github.com/fsfe/reuse-tool/issues/586#issuecomment-1310425706 + # + if ( + args.add_license_concluded + and args.creator_person is None + and args.creator_organization is None + ): + print( + _( + "error: --creator-person=NAME or --creator-organization=NAME" + " required when --add-license-concluded is provided" + ), + file=sys.stderr, + ) + return 1 + with contextlib.ExitStack() as stack: if args.file: out = stack.enter_context(args.file.open("w", encoding="utf-8")) @@ -57,6 +86,11 @@ def run(args, project: Project, out=sys.stdout) -> int: add_license_concluded=args.add_license_concluded, ) - out.write(report.bill_of_materials()) + out.write( + report.bill_of_materials( + creator_person=args.creator_person, + creator_organization=args.creator_organization, + ) + ) return 0 diff --git a/tests/test_main.py b/tests/test_main.py index 9ecde6c57..f6486c80b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -283,25 +283,66 @@ def test_spdx(fake_repository, stringio): # Ensure no LicenseConcluded is included without the flag assert "\nLicenseConcluded: NOASSERTION\n" in output assert "\nLicenseConcluded: GPL-3.0-or-later\n" not in output + assert "\nCreator: Person: Anonymous ()\n" in output + assert "\nCreator: Organization: Anonymous ()\n" in output # TODO: This test is rubbish. assert result == 0 assert output +def test_spdx_creator_info(fake_repository, stringio): + os.chdir(str(fake_repository)) + result = main( + [ + "spdx", + "--creator-person=Jane Doe (jane.doe@example.org)", + "--creator-organization=FSFE", + ], + out=stringio, + ) + output = stringio.getvalue() + + assert result == 0 + assert "\nCreator: Person: Jane Doe (jane.doe@example.org)\n" in output + assert "\nCreator: Organization: FSFE ()\n" in output + + def test_spdx_add_license_concluded(fake_repository, stringio): - """Compile to an SPDX document.""" + """Compile to an SPDX document with the LicenseConcluded field.""" os.chdir(str(fake_repository)) - result = main(["spdx", "--add-license-concluded"], out=stringio) + result = main( + [ + "spdx", + "--add-license-concluded", + "--creator-person=Jane Doe", + "--creator-organization=FSFE", + ], + out=stringio, + ) output = stringio.getvalue() # Ensure no LicenseConcluded is included without the flag + assert result == 0 assert "\nLicenseConcluded: NOASSERTION\n" not in output assert "\nLicenseConcluded: GPL-3.0-or-later\n" in output + assert "\nCreator: Person: Jane Doe ()\n" in output + assert "\nCreator: Organization: FSFE ()\n" in output - # TODO: This test is rubbish. - assert result == 0 - assert output + +def test_spdx_add_license_concluded_without_creator_info( + fake_repository, stringio +): + """Adding LicenseConcluded should require creator information""" + os.chdir(str(fake_repository)) + result = main(["spdx", "--add-license-concluded"], out=stringio) + output = stringio.getvalue() + + assert result == 1 + # An error message is emitted, but on stderr to avoid returning mangled + # output when the caller expects an SPDX document. We cannot assert its + # contents because of that, unfortunately. + assert not output def test_spdx_no_multiprocessing(fake_repository, stringio, multiprocessing):