From f0402d2b39384ff0a55b71e5aadfea0f21993eb4 Mon Sep 17 00:00:00 2001 From: Frederic Leger Date: Tue, 21 Nov 2023 15:52:49 +0100 Subject: [PATCH] Added a missing package class. - Modified the Document.add_package() method to take a package for input, not many parameters which are used to build a package in the end. - Updated the tests for full coverage of the SPDX module, and to take into account the modifications of the add_package() method. - Added more documentation for Package class fields. - Added package comments to the package class. - Added links to the specification for the package fields. --- src/e3/spdx.py | 211 +++++++++++++++++++----------------- tests/tests_e3/spdx_test.py | 185 +++++++++++++++++++++---------- 2 files changed, 241 insertions(+), 155 deletions(-) diff --git a/src/e3/spdx.py b/src/e3/spdx.py index 047a4245..266e4b25 100644 --- a/src/e3/spdx.py +++ b/src/e3/spdx.py @@ -301,7 +301,8 @@ class Tool(Entity): class PackageName(SPDXEntryStr): """Identify the full name of the package. - See 7.1 Package name field + See 7.1 `Package name field + `_ """ json_entry_key = "name" @@ -310,7 +311,8 @@ class PackageName(SPDXEntryStr): class PackageVersion(SPDXEntryStr): """Identify the version of the package. - See 7.3 Package version field + See 7.3 `Package version field + `_ """ json_entry_key = "versionInfo" @@ -319,14 +321,16 @@ class PackageVersion(SPDXEntryStr): class PackageFileName(SPDXEntryStr): """Provide the actual file name of the package. - See 7.4 Package file name field + See 7.4 `Package file name field + `_ """ class PackageSupplier(EntityRef): """Identify the actual distribution source for the package. - See 7.5 Package supplier field + See 7.5 `Package supplier field + `_ """ json_entry_key = "supplier" @@ -335,7 +339,8 @@ class PackageSupplier(EntityRef): class PackageOriginator(EntityRef): """Identify from where the package originally came. - See 7.6 Package originator field + See 7.6 `Package originator field + `_ """ json_entry_key = "originator" @@ -344,7 +349,8 @@ class PackageOriginator(EntityRef): class PackageDownloadLocation(SPDXEntryMaybeStr): """Identifies the download location of the package. - See 7.7 Package download location field + See 7.7 `Package download location field + `_ """ json_entry_key = "downloadLocation" @@ -353,14 +359,16 @@ class PackageDownloadLocation(SPDXEntryMaybeStr): class FilesAnalyzed(SPDXEntryBool): """Indicates whether the file content of this package have been analyzed. - See 7.8 Files analyzed field + See 7.8 `Files analyzed field + `_ """ class PackageChecksum(SPDXEntryStr, metaclass=ABCMeta): """Provide a mechanism that permits unique identification of the package. - See 7.10 Package checksum field + See 7.10 `Package checksum field + `_ """ entry_key = "PackageChecksum" @@ -383,6 +391,16 @@ def to_json_dict(self) -> dict[str, dict[str, str]]: } +class PackageHomePage(SPDXEntryMaybeStr): + """Identifies the homepage location of the package. + + See 7.11 `Package home page field + `_ + """ + + json_entry_key = "homePage" + + class SHA1(PackageChecksum): algorithm = "SHA1" @@ -394,47 +412,51 @@ class SHA256(PackageChecksum): class PackageLicenseConcluded(SPDXEntryMaybeStr): """Contain the license concluded as governing the package. - See 7.13 Concluded license field + See 7.13 `Concluded license field + `_ """ json_entry_key = "licenseConcluded" -class PackageHomePage(SPDXEntryMaybeStr): - """Identifies the download location of the package. +class PackageLicenseDeclared(SPDXEntryMaybeStr): + """Contain the license having been declared by the authors of the package. - See 7.11 `Package home page field - `_ + See 7.15 `Declared license field + `_ """ - json_entry_key = "homePage" + json_entry_key = "licenseDeclared" class PackageLicenseComments(SPDXEntryMaybeStrMultilines): - """Cecord background information or analysis for the Concluded License. + """Record background information or analysis for the Concluded License. - See 7.16 Comments on license field + See 7.16 `Comments on license field + `_ """ json_entry_key = "licenseComments" -class PackageLicenseDeclared(SPDXEntryMaybeStr): - """Contain the license having been declared by the authors of the package. +class PackageCopyrightText(SPDXEntryMaybeStrMultilines): + """Identify the copyright holders of the package. - See 7.15 Declared license field + See 7.17 `Copyright text field + `_ """ - json_entry_key = "licenseDeclared" + json_entry_key = "copyrightText" -class PackageCopyrightText(SPDXEntryMaybeStrMultilines): - """Identify the copyright holders of the package. +class PackageComment(SPDXEntryMaybeStrMultilines): + """Record background information or analysis for the Concluded License. - See 7.17 Copyright text field + See 7.20 `Package comment field + `_ """ - json_entry_key = "copyrightText" + json_entry_key = "comments" class ExternalRefCategory(Enum): @@ -473,7 +495,8 @@ class ExternalRefCategory(Enum): class ExternalRef(SPDXEntry): """Reference an external source of information relevant to the package. - See 7.21 External reference field + See 7.21 `External reference field + `_ """ json_entry_key = "externalRefs" @@ -674,20 +697,72 @@ class Package(SPDXSection): """Describe a package.""" name: PackageName + """Name of this package. See :class:`PackageName`.""" spdx_id: SPDXID + """Unique ID of this package. Generally made of ``f"{name}-{version}"``.""" version: PackageVersion + """Version of this package. See :class:`PackageVersion`.""" file_name: PackageFileName + """Package file name. See :class:`PackageFileName`.""" checksum: list[PackageChecksum] + """A list of package checksums. See :class:`PackageChecksum`. + + The only supported checksum algorithms (for now) are :class:`SHA1` and + :class:`SHA256`. + """ supplier: PackageSupplier + """The package supplier. See :class:`PackageSupplier`""" originator: PackageOriginator + """The package originator (if any). See :class:`PackageOriginator`.""" copyright_text: PackageCopyrightText + """The package copyright text (if any). See :class:`PackageCopyrightTest`.""" files_analyzed: FilesAnalyzed + """Whether the files of this package have been analyzed. + + See :class:`FilesAnalyzed`. + """ license_concluded: PackageLicenseConcluded + """The license concluded for this package. + + See :class:`PackageLicenseConcluded`. + """ license_comments: PackageLicenseComments | None + """The license comments for this package. + + See :class:`PackageLicenseComments`. + """ license_declared: PackageLicenseDeclared | None + """The license declared for this package. + + See :class:`PackageLicenseDeclared`. + """ homepage: PackageHomePage | None + """The home page of this package (if any). See :class:`PackageHomePage`.""" download_location: PackageDownloadLocation + """The package download location (URL) of this package (if any). + + See :class:`PackageDownloadLocation`. + """ external_refs: list[ExternalRef] | None + """A list of external references for this package. + + For instance: + + .. code-block:: python + + ExternalRef( + reference_category=ExternalRefCategory.package_manager, + reference_type="purl", + reference_locator="pkg:generic/my-dep@1b2", + ) + + .. seealso:: :class:`PackageLicenseConcluded`. + """ + comment: PackageComment | None = field(default=None) + """Any useful comment associated to this package. + + .. seealso:: :class:`PackageComment` + """ @dataclass @@ -747,43 +822,14 @@ def spdx_id(self) -> SPDXID: def add_package( self, - name: str, - version: str, - file_name: str, - checksum: list[PackageChecksum], - license_concluded: str, - supplier: Entity | Literal["NOASSERTION"], - originator: Entity | Literal["NOASSERTION"], - download_location: str, - files_analyzed: bool, - copyright_text: str, - license_comments: str | None = None, - license_declared: str | None = None, + package: Package, is_main_package: bool = False, add_relationship: bool = True, - external_refs: list[ExternalRef] | None = None, - homepage: str | None = None, ) -> SPDXID: """Add a new Package and describe its relationship to other elements. - :param name: the full name of this package - :param version: the package version - :param file_name: the actual file name of this package - :param checksum: the package checksum (see SHA1, SHA256 classes) - :param license_concluded: the license concluded as govering the package - :param license_comments: comments for the license_concluded field - :param license_declared: the license declared in the package - :param supplier: actual distribution source for the package - :param originator: this field identifies from where or whom the package - originally came - :param homepage: The website that serves as the package's home page. - :param download_location: download URL for the package at the time that - the SPDX document was created - :param files_analyzed: whether the file content of this package has - been available for or subjected to analysis when creating the + :param package: An already created :class:`Package` to be added to this SPDX document - :param copyright_text: identify the copyright holders of the package, - as well as any dates present :param is_main_package: whether the package is the main package, in which case a relationship will automatically be added to record that the document DESCRIBES this package. If false, it is assumed @@ -792,59 +838,26 @@ def add_package( :param add_relationship: whether to automatically add a relationship element - either (DOCUMENT DESCRIBES
) if is_main_package is True or (
CONTAINS ) - :param external_refs: A list of `ExternalRef` object representing the - list of reference to external source of additional information, - metadata, enumerations, asset identifiers, or downloadable content - believed to be relevant to the Package. :return: the package SPDX_ID - """ - if not name.endswith(version): - new_package_spdx_id = f"{name}-{version}" - else: - new_package_spdx_id = name + """ # noqa RST304 + if is_main_package and not package.spdx_id.value.endswith("-pkg"): + package.spdx_id = SPDXID(f"{package.spdx_id.value}-pkg") - if is_main_package: - # This is the main package, given that is often occurs that - # a main package depends on a source package of the same name - # appends a "-pkg" suffix - new_package_spdx_id += "-pkg" - - new_package = Package( - name=PackageName(name), - spdx_id=SPDXID(new_package_spdx_id), - version=PackageVersion(version), - file_name=PackageFileName(file_name), - checksum=checksum, - license_concluded=PackageLicenseConcluded(license_concluded), - license_comments=PackageLicenseComments(license_comments) - if license_comments is not None - else None, - license_declared=PackageLicenseDeclared(license_declared) - if license_declared is not None - else None, - supplier=PackageSupplier(supplier), - originator=PackageOriginator(originator), - homepage=PackageHomePage(homepage) if homepage is not None else None, - download_location=PackageDownloadLocation(download_location), - files_analyzed=FilesAnalyzed(files_analyzed), - copyright_text=PackageCopyrightText(copyright_text), - external_refs=external_refs, - ) - if new_package.spdx_id in self.packages: + if package.spdx_id in self.packages: raise InvalidSPDX( - f"A package with the same SPDXID {new_package.spdx_id}" + f"A package with the same SPDXID {package.spdx_id}" " has already been added" ) if is_main_package: - self.main_package_spdx_id = new_package.spdx_id + self.main_package_spdx_id = package.spdx_id if add_relationship: if is_main_package: relationship = Relationship( spdx_element_id=self.spdx_id, relationship_type=RelationshipType.DESCRIBES, - related_spdx_element=new_package.spdx_id, + related_spdx_element=package.spdx_id, ) else: if self.main_package_spdx_id is None: @@ -852,12 +865,12 @@ def add_package( relationship = Relationship( spdx_element_id=self.main_package_spdx_id, relationship_type=RelationshipType.CONTAINS, - related_spdx_element=new_package.spdx_id, + related_spdx_element=package.spdx_id, ) self.relationships.append(relationship) - self.packages[new_package.spdx_id] = new_package - return new_package.spdx_id + self.packages[package.spdx_id] = package + return package.spdx_id def add_relationship(self, relationship: Relationship) -> None: """Add a new relationship to the document. diff --git a/tests/tests_e3/spdx_test.py b/tests/tests_e3/spdx_test.py index af74af22..51f2fa1a 100644 --- a/tests/tests_e3/spdx_test.py +++ b/tests/tests_e3/spdx_test.py @@ -1,15 +1,29 @@ from e3.spdx import ( Document, + EntityRef, ExternalRef, ExternalRefCategory, + FilesAnalyzed, Creator, Organization, Tool, + Package, + PackageComment, + PackageCopyrightText, + PackageDownloadLocation, + PackageFileName, + PackageLicenseComments, + PackageLicenseConcluded, + PackageLicenseDeclared, + PackageName, PackageOriginator, PackageSupplier, + PackageVersion, Person, SHA1, SHA256, + SPDXID, + SPDXEntryMaybeStrMultilines, NOASSERTION, Relationship, RelationshipType, @@ -22,6 +36,9 @@ def test_entities_ref_spdx(): org = Organization("AdaCore") assert org.to_tagvalue() == "Organization: AdaCore" + assert "AdaCore" in str(org) + assert "NOASSERTION" in str(Organization(NOASSERTION)) + assert Organization(NOASSERTION).to_json_dict() == {"organization": "NOASSERTION"} assert Creator(org).to_tagvalue() == "Creator: Organization: AdaCore" @@ -33,6 +50,19 @@ def test_entities_ref_spdx(): ) +def test_entity_ref() -> None: + """Tests for the EntiryRef class which are not covered by the other tests.""" + org: EntityRef = EntityRef(Organization("AdaCore")) + no_assertion: EntityRef = EntityRef(NOASSERTION) + + assert org.to_tagvalue() == "EntityRef: Organization: AdaCore" + assert no_assertion.to_tagvalue() == "EntityRef: NOASSERTION" + assert str(no_assertion) == "NOASSERTION" + assert str(org) == "Organization: AdaCore" + assert no_assertion.to_json_dict() == {"entityRef": "NOASSERTION"} + assert org.to_json_dict() == {"entityRef": "Organization: AdaCore"} + + def test_external_ref(): value = { "referenceType": "purl", @@ -62,41 +92,47 @@ def test_spdx(): Person("e3-core maintainer"), ], ) - - doc.add_package( - name="my-spdx-test-main", - version="2.2.2", - file_name="main-pkg.zip", + package: Package = Package( + name=PackageName("my-spdx-test-main"), + version=PackageVersion("2.2.2"), + spdx_id=SPDXID("my-spdx-test-main-2.2.2"), + file_name=PackageFileName("main-pkg.zip"), checksum=[ SHA1("6476df3aac780622368173fe6e768a2edc3932c8"), SHA256( "91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada", ), ], - license_concluded="GPL-3.0-or-later", - license_declared="GPL-3.0-or-later", - supplier=Organization("AdaCore"), - originator=Organization("AdaCore"), - download_location=NOASSERTION, - files_analyzed=False, - copyright_text="2023 AdaCore", - is_main_package=True, + license_concluded=PackageLicenseConcluded("GPL-3.0-or-later"), + license_declared=PackageLicenseDeclared("GPL-3.0-or-later"), + license_comments=None, + supplier=PackageSupplier(Organization("AdaCore")), + originator=PackageOriginator(Organization("AdaCore")), + download_location=PackageDownloadLocation(NOASSERTION), + files_analyzed=FilesAnalyzed(False), + copyright_text=PackageCopyrightText("2023 AdaCore"), + external_refs=None, + homepage=None, ) - doc.add_package( - name="my-dep", - version="1b2", - file_name="my-dep-1b2.tgz", + doc.add_package(package, is_main_package=True) + + package = Package( + name=PackageName("my-dep"), + version=PackageVersion("1b2"), + spdx_id=SPDXID("my-dep-1b2"), + file_name=PackageFileName("my-dep-1b2.tgz"), checksum=[ SHA1("6876df3aa8780622368173fe6e868a2edc3932c8"), ], - license_concluded="GPL-3.0-or-later", - license_comments="Pretty sure this is GPL v3", - supplier=Organization("AdaCore"), - originator=Organization("AdaCore"), - download_location=NOASSERTION, - files_analyzed=False, - copyright_text="2023 AdaCore", + license_concluded=PackageLicenseConcluded("GPL-3.0-or-later"), + license_declared=None, + license_comments=PackageLicenseComments("Pretty sure this is GPL v3"), + supplier=PackageSupplier(Organization("AdaCore")), + originator=PackageOriginator(Organization("AdaCore")), + download_location=PackageDownloadLocation(NOASSERTION), + files_analyzed=FilesAnalyzed(False), + copyright_text=PackageCopyrightText("2023 AdaCore"), external_refs=[ ExternalRef( reference_category=ExternalRefCategory.package_manager, @@ -104,23 +140,34 @@ def test_spdx(): reference_locator="pkg:generic/my-dep@1b2", ) ], + homepage=None, + comment=PackageComment("A very useful comment on that package !"), ) - pkg_id = doc.add_package( - name="my-dep2", - version="1c3", - file_name="my-dep2-1c3.tgz", + + doc.add_package(package) + + package = Package( + name=PackageName("my-dep2"), + version=PackageVersion("1c3"), + spdx_id=SPDXID("my-dep2-1c3"), + file_name=PackageFileName("my-dep2-1c3.tgz"), checksum=[ SHA1("6176df3aa1710633361173fe6e161a3edd3933d1"), ], - license_concluded="GPL-3.0-or-later", - supplier=Organization("AdaCore"), - originator=Organization("AdaCore"), - download_location=NOASSERTION, - files_analyzed=False, - copyright_text="2023 AdaCore", - add_relationship=False, + license_concluded=PackageLicenseConcluded("GPL-3.0-or-later"), + license_declared=None, + license_comments=None, + supplier=PackageSupplier(Organization("AdaCore")), + originator=PackageOriginator(Organization("AdaCore")), + download_location=PackageDownloadLocation(NOASSERTION), + files_analyzed=FilesAnalyzed(False), + copyright_text=PackageCopyrightText("2023 AdaCore"), + external_refs=None, + homepage=None, ) + pkg_id = doc.add_package(package, add_relationship=False) + doc.add_relationship( relationship=Relationship( spdx_element_id=pkg_id, @@ -207,6 +254,7 @@ def test_spdx(): "PackageLicenseComments: Pretty sure this is GPL v3", "PackageDownloadLocation: NOASSERTION", "ExternalRef: PACKAGE-MANAGER purl pkg:generic/my-dep@1b2", + "PackageComment: A very useful comment on that package !", "", "", "# Package", @@ -286,6 +334,7 @@ def test_spdx(): "checksumValue": "6876df3aa8780622368173fe6e868a2edc3932c8", } ], + "comments": "A very useful comment on that package !", "copyrightText": "2023 AdaCore", "downloadLocation": "NOASSERTION", "externalRefs": [ @@ -337,24 +386,29 @@ def test_invalid_spdx(): ) def add_main(is_main_package): - return doc.add_package( - name="my-spdx-test-main", - version="2.2.2", - file_name="main-pkg.zip", + package: Package = Package( + name=PackageName("my-spdx-test-main"), + version=PackageVersion("2.2.2"), + spdx_id=SPDXID("my-spdx-test-main-2.2.2"), + file_name=PackageFileName("main-pkg.zip"), checksum=[ SHA1("6476df3aac780622368173fe6e768a2edc3932c8"), SHA256( "91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada", ), ], - license_concluded="GPL-3.0-or-later", - supplier=Organization("AdaCore"), - originator=Organization("AdaCore"), - download_location=NOASSERTION, - files_analyzed=False, - copyright_text="2023 AdaCore", - is_main_package=is_main_package, + license_concluded=PackageLicenseConcluded("GPL-3.0-or-later"), + license_declared=PackageLicenseDeclared("GPL-3.0-or-later"), + license_comments=None, + supplier=PackageSupplier(Organization("AdaCore")), + originator=PackageOriginator(Organization("AdaCore")), + download_location=PackageDownloadLocation(NOASSERTION), + files_analyzed=FilesAnalyzed(False), + copyright_text=PackageCopyrightText("2023 AdaCore"), + external_refs=None, + homepage=None, ) + return doc.add_package(package, is_main_package=is_main_package) with pytest.raises(InvalidSPDX) as err: add_main(is_main_package=False) @@ -369,21 +423,40 @@ def add_main(is_main_package): name = "my-dep" else: name = "my___-dep" - doc.add_package( - name=name, - version="1b2", - file_name="my-dep-1b2.tgz", + dep: Package = Package( + name=PackageName(name), + version=PackageVersion("1b2"), + spdx_id=SPDXID("my-dep-1b2"), + file_name=PackageFileName("my-dep-1b2.tgz"), checksum=[ SHA1("6876df3aa8780622368173fe6e868a2edc3932c8"), ], - license_concluded="GPL-3.0-or-later", - supplier=Organization("AdaCore"), - originator=Organization("AdaCore"), - download_location=NOASSERTION, - files_analyzed=False, - copyright_text="2023 AdaCore", + license_concluded=PackageLicenseConcluded("GPL-3.0-or-later"), + license_declared=None, + license_comments=None, + supplier=PackageSupplier(Organization("AdaCore")), + originator=PackageOriginator(Organization("AdaCore")), + download_location=PackageDownloadLocation(NOASSERTION), + files_analyzed=FilesAnalyzed(False), + copyright_text=PackageCopyrightText("2023 AdaCore"), + external_refs=None, + homepage=None, ) + doc.add_package(dep) assert ( "A package with the same SPDXID SPDXRef-my-dep-1b2 has already been added" in str(err) ) + + +def test_spdx_entry_maybe_str_multilines() -> None: + """SPDXEntryMaybeStrMultilines class tests. + + Tests for the SPDXEntryMaybeStrMultilines class which are not covered by + the other tests. + """ + ml: SPDXEntryMaybeStrMultilines = SPDXEntryMaybeStrMultilines("value") + no_assertion: SPDXEntryMaybeStrMultilines = SPDXEntryMaybeStrMultilines(NOASSERTION) + + assert ml.to_tagvalue() == "SPDXEntryMaybeStrMultilines: value" + assert no_assertion.to_tagvalue() == "SPDXEntryMaybeStrMultilines: NOASSERTION"