From 6353b3dd80a870783e2008d4139e5462a595f1ca 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 | 23 +++++++++++++++++--- src/reuse/spdx.py | 36 ++++++++++++++++++++++++++++++- tests/test_main.py | 52 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/reuse/report.py b/src/reuse/report.py index 86c1dd5e6..d6849a988 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: {format_creator(creator_person)}\n") + out.write( + f"Creator: Organization: {format_creator(creator_organization)}\n" + ) out.write(f"Creator: Tool: reuse-{__version__}\n") now = datetime.datetime.utcnow() @@ -420,3 +427,13 @@ def __hash__(self): if self.spdxfile.chk_sum is not None: return hash(self.spdxfile.name + self.spdxfile.chk_sum) return super().__hash__(self) + + +def format_creator(creator): + """Render the creator field based on the provided flag""" + if creator is None: + return "Anonymous ()" + if "(" in creator and creator.endswith(")"): + # The creator field already contains an email address + return creator + return creator + " ()" 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..b9bfd9317 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -283,25 +283,67 @@ 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): + """Ensure the --creator-* flags are properly formatted""" + 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):