From 85e16d0668c778ddb40b7a7b7aa0130e239ada80 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 23 Jun 2026 16:26:23 +0100 Subject: [PATCH 1/6] Read SPDX 3 by spdx-python-model Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 7 + pyproject.toml | 1 + src/spdx_tools/spdx3/formats.py | 21 + src/spdx_tools/spdx3/parser/__init__.py | 5 + .../spdx3/parser/json_ld/__init__.py | 2 + .../spdx3/parser/json_ld/json_ld_parser.py | 20 + src/spdx_tools/spdx3/parser/parse_anything.py | 17 + tests/spdx3/data/example.spdx3.json | 426 ++++++++++++++++++ tests/spdx3/parser/__init__.py | 2 + tests/spdx3/parser/test_read.py | 38 ++ 10 files changed, 539 insertions(+) create mode 100644 src/spdx_tools/spdx3/formats.py create mode 100644 src/spdx_tools/spdx3/parser/__init__.py create mode 100644 src/spdx_tools/spdx3/parser/json_ld/__init__.py create mode 100644 src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py create mode 100644 src/spdx_tools/spdx3/parser/parse_anything.py create mode 100644 tests/spdx3/data/example.spdx3.json create mode 100644 tests/spdx3/parser/__init__.py create mode 100644 tests/spdx3/parser/test_read.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 797be5ba9..e5689c4ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## (unreleased) + +### New features and changes + +* experimental SPDX 3.0 support, built on the `spdx-python-model` bindings: + * read a SPDX 3 JSON-LD file into memory via `spdx_tools.spdx3.parser.parse_file` + ## v0.8.5 (2026-03-13) ### New features and changes diff --git a/pyproject.toml b/pyproject.toml index e773d841c..791439114 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "pyyaml", "rdflib", "semantic_version", + "spdx-python-model @ git+https://github.com/spdx/spdx-python-model.git@main", "uritools", "xmltodict", ] diff --git a/src/spdx_tools/spdx3/formats.py b/src/spdx_tools/spdx3/formats.py new file mode 100644 index 000000000..3a770ae4f --- /dev/null +++ b/src/spdx_tools/spdx3/formats.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 +from enum import Enum, auto + +from spdx_tools.spdx.parser.error import SPDXParsingError + + +class FileFormat(Enum): + JSON_LD = auto() + + +def file_name_to_format(file_name: str) -> FileFormat: + # SPDX 3.0 currently defines a single serialization: JSON-LD. + # Common extensions are "spdx3.json", ".json" and ".jsonld". + # See: + # https://github.com/OpenChain-Project/Telco-WG/blob/main/OpenChain-Telco-SBOM-Guide_1.2_DRAFT_EN.md + # https://www.iana.org/assignments/media-types/application/spdx3+json + if file_name.endswith(".json") or file_name.endswith(".jsonld"): + return FileFormat.JSON_LD + else: + raise SPDXParsingError(["Unsupported SPDX 3 file type: " + str(file_name)]) diff --git a/src/spdx_tools/spdx3/parser/__init__.py b/src/spdx_tools/spdx3/parser/__init__.py new file mode 100644 index 000000000..631f0ae42 --- /dev/null +++ b/src/spdx_tools/spdx3/parser/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 +from spdx_tools.spdx3.parser.parse_anything import parse_file + +__all__ = ["parse_file"] diff --git a/src/spdx_tools/spdx3/parser/json_ld/__init__.py b/src/spdx_tools/spdx3/parser/json_ld/__init__.py new file mode 100644 index 000000000..2173e3181 --- /dev/null +++ b/src/spdx_tools/spdx3/parser/json_ld/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py b/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py new file mode 100644 index 000000000..bc27565c2 --- /dev/null +++ b/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 +from spdx_python_model import v3_0_1 as spdx_3_0 + +from spdx_tools.spdx.parser.error import SPDXParsingError + + +def parse_from_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectSet: + """Read a SPDX 3 JSON-LD file into a SHACLObjectSet (the in-memory representation + provided by the spdx-python-model bindings).""" + object_set = spdx_3_0.SHACLObjectSet() + try: + # The binding's deserializer reads from a binary stream. + with open(file_name, "rb") as file: + spdx_3_0.JSONLDDeserializer().read(file, object_set) + except OSError as err: + raise SPDXParsingError([f"Could not open file {file_name}: {err}"]) + except Exception as err: + raise SPDXParsingError([f"Error while parsing {file_name}: {err}"]) + return object_set diff --git a/src/spdx_tools/spdx3/parser/parse_anything.py b/src/spdx_tools/spdx3/parser/parse_anything.py new file mode 100644 index 000000000..038c467da --- /dev/null +++ b/src/spdx_tools/spdx3/parser/parse_anything.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 +from spdx_python_model import v3_0_1 as spdx_3_0 + +from spdx_tools.spdx3.formats import FileFormat, file_name_to_format +from spdx_tools.spdx3.parser.json_ld import json_ld_parser + + +def parse_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectSet: + """Parse a SPDX 3 file into a SHACLObjectSet, dispatching on the file format. + + SPDX 3.0 currently defines a single serialization (JSON-LD); the dispatch is + kept to mirror the SPDX 2 ``parse_file`` API and to ease adding formats later. + """ + input_format = file_name_to_format(file_name) + if input_format == FileFormat.JSON_LD: + return json_ld_parser.parse_from_file(file_name, encoding) diff --git a/tests/spdx3/data/example.spdx3.json b/tests/spdx3/data/example.spdx3.json new file mode 100644 index 000000000..7dd85c050 --- /dev/null +++ b/tests/spdx3/data/example.spdx3.json @@ -0,0 +1,426 @@ +{ + "@context" : "https://spdx.org/rdf/3.0.1/spdx-context.jsonld", + "@graph" : [ { + "@id" : "_:creationInfo_0", + "type" : "CreationInfo", + "specVersion" : "3.0.1", + "createdBy" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd0" ], + "createdUsing" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd2", "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd1" ], + "created" : "2024-11-18T10:22:12Z" + }, { + "@id" : "_:creationInfo_1", + "type" : "CreationInfo", + "specVersion" : "3.0.1", + "createdBy" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd40" ], + "createdUsing" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd41", "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd42" ], + "created" : "2024-11-18T10:22:12Z" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd3", + "type" : "Relationship", + "relationshipType" : "describes", + "completeness" : "noAssertion", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/document0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd5", + "type" : "Relationship", + "relationshipType" : "contains", + "completeness" : "noAssertion", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd6" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd7", + "type" : "Relationship", + "relationshipType" : "generates", + "completeness" : "noAssertion", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd6", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd8", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd6", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd10", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd6", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd11", + "type" : "Relationship", + "relationshipType" : "contains", + "completeness" : "noAssertion", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd12" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd13", + "type" : "Relationship", + "relationshipType" : "hasTestCase", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd12" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd14", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd12", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd15", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd12", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd16", + "type" : "Relationship", + "relationshipType" : "hasDynamicLink", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd17", + "comment" : "Relationship based on Maven POM file dependency information", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd18", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd17", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd20", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd17", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd21", + "type" : "Relationship", + "relationshipType" : "hasDynamicLink", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd22", + "comment" : "Relationship based on Maven POM file dependency information", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd23", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd22", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd24", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd22", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd25", + "type" : "Relationship", + "relationshipType" : "hasDynamicLink", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd26", + "comment" : "Relationship based on Maven POM file dependency information", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd27", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd26", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd28", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd26", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd29", + "type" : "Relationship", + "relationshipType" : "hasDynamicLink", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd30", + "comment" : "Relationship based on Maven POM file dependency information", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd31", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd30", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd32", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd30", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd33", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd36", + "type" : "Relationship", + "relationshipType" : "hasDistributionArtifact", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd35" ], + "completeness" : "complete", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd38", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd39", + "type" : "Relationship", + "relationshipType" : "amendedBy", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/document0" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1#SPDXRef-DOCUMENT", + "comment" : "The original document and been enriched by the Parlay application", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd46", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd44", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd48", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd49" ], + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd44", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/document0", + "type" : "SpdxDocument", + "dataLicense" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd43", + "rootElement" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4" ], + "import" : [ { + "type" : "ExternalMap", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "3f9deeef2efdbb0eb4b15ec216f5c4e3af2d13e2" + } ], + "externalSpdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1#SPDXRef-DOCUMENT", + "locationHint" : "http://spdx.org/spdxdocs/examplemaven-0.0.1" + } ], + "name" : "examplemaven", + "namespaceMap" : [ { + "type" : "NamespaceMap", + "prefix" : "DocumentRef-original", + "namespace" : "http://spdx.org/spdxdocs/examplemaven-0.0.1#" + } ], + "creationInfo" : "_:creationInfo_1" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd1", + "type" : "Tool", + "name" : "spdx-maven-plugin", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd2", + "type" : "Tool", + "name" : "Parlay", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd41", + "type" : "Tool", + "name" : "spdx-maven-plugin", + "creationInfo" : "_:creationInfo_1" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/additionalToolSPDXRef-gnrtd42", + "type" : "Tool", + "name" : "Parlay", + "creationInfo" : "_:creationInfo_1" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd9", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "Apache-2.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd19", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "NOASSERTION", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd43", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "CC0-1.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd49", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "CPL-1.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd45", + "type" : "LifecycleScopedRelationship", + "relationshipType" : "dependsOn", + "scope" : "test", + "to" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd44" ], + "completeness" : "noAssertion", + "from" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "comment" : "Relationship created based on Maven POM information", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd34", + "type" : "Organization", + "name" : "Linux Foundation", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd37", + "type" : "Organization", + "name" : "SPDX", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd47", + "type" : "Organization", + "name" : "JUnit", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd0", + "type" : "Person", + "name" : "Gary O'Neall", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd40", + "type" : "Person", + "name" : "Gary O'Neall", + "creationInfo" : "_:creationInfo_1" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd6", + "type" : "software_File", + "software_copyrightText" : "Copyright (c) 2020 Source Auditor Inc.", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "a6f47dbc7e4615058490055172fe0065c55f8fc5" + } ], + "software_attributionText" : [ "SPDX-License-Identifier: Apache-2.0\nCopyright (c) 2022 Source Auditor Inc." ], + "name" : "./src/main/java/org/spdx/examplemaven/App.java", + "software_primaryPurpose" : "source", + "comment" : "This file contains SPDX-License-Identifiers for Apache-2.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd12", + "type" : "software_File", + "software_copyrightText" : "Copyright (c) 2020 Source Auditor Inc.", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "4b4df52d36588c8e9482d56eebc42336447f3dad" + } ], + "software_attributionText" : [ "SPDX-License-Identifier: Apache-2.0\nCopyright (c) 2022 Source Auditor Inc." ], + "name" : "./src/test/java/org/spdx/examplemaven/AppTest.java", + "software_primaryPurpose" : "source", + "comment" : "This file contains SPDX-License-Identifiers for Apache-2.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd35", + "type" : "software_File", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "b8a7e6c75001e6d78625cfc9a3103bf121abf8b4" + } ], + "name" : "examplemaven-0.0.1.jar", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd4", + "type" : "software_Package", + "software_copyrightText" : "Copyright (c) 2022 Source Auditor Inc.", + "suppliedBy" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd37", + "verifiedUsing" : [ { + "type" : "PackageVerificationCode", + "algorithm" : "sha1", + "hashValue" : "c12417def36d7804096521de4280721e5863e68b" + } ], + "name" : "examplemaven", + "software_primaryPurpose" : "library", + "software_downloadLocation" : "NOASSERTION", + "summary" : "This is a simple example Maven project created using the Maven quickstart archetype with one dependency added.", + "software_packageVersion" : "0.0.1", + "software_homePage" : "https://github.com/spdx/spdx-examples", + "description" : "This is a simple example Maven project created using the Maven quickstart archetype with one dependency added.", + "originatedBy" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd34" ], + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd17", + "type" : "software_Package", + "software_copyrightText" : "UNSPECIFIED", + "software_downloadLocation" : "NOASSERTION", + "summary" : "The Apache Log4j Implementation", + "description" : "The Apache Log4j Implementation", + "name" : "Apache Log4j Core", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd22", + "type" : "software_Package", + "software_copyrightText" : "UNSPECIFIED", + "software_downloadLocation" : "NOASSERTION", + "summary" : "The Apache Log4j API", + "description" : "The Apache Log4j API", + "name" : "Apache Log4j API", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd26", + "type" : "software_Package", + "software_copyrightText" : "UNSPECIFIED", + "software_downloadLocation" : "NOASSERTION", + "summary" : "The slf4j API", + "software_homePage" : "http://www.slf4j.org", + "description" : "The slf4j API", + "name" : "SLF4J API Module", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd30", + "type" : "software_Package", + "software_copyrightText" : "UNSPECIFIED", + "software_downloadLocation" : "NOASSERTION", + "summary" : "The Apache Log4j SLF4J API binding to Log4j 2 Core", + "description" : "The Apache Log4j SLF4J API binding to Log4j 2 Core", + "name" : "Apache Log4j SLF4J Binding", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd44", + "type" : "software_Package", + "software_copyrightText" : "UNSPECIFIED", + "software_downloadLocation" : "NOASSERTION", + "summary" : "JUnit is a regression testing framework written by Erich Gamma and Kent Beck. It is used by the developer who implements unit tests in Java.", + "software_packageVersion" : "3.8.1", + "software_homePage" : "http://junit.org", + "description" : "JUnit is a regression testing framework written by Erich Gamma and Kent Beck. It is used by the developer who implements unit tests in Java.", + "name" : "JUnit", + "originatedBy" : [ "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd47" ], + "creationInfo" : "_:creationInfo_0" + } ] +} \ No newline at end of file diff --git a/tests/spdx3/parser/__init__.py b/tests/spdx3/parser/__init__.py new file mode 100644 index 000000000..2173e3181 --- /dev/null +++ b/tests/spdx3/parser/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/spdx3/parser/test_read.py b/tests/spdx3/parser/test_read.py new file mode 100644 index 000000000..1ebf8c226 --- /dev/null +++ b/tests/spdx3/parser/test_read.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 +import os + +import pytest +from spdx_python_model import v3_0_1 as spdx_3_0 + +from spdx_tools.spdx3.parser import parse_file +from spdx_tools.spdx.parser.error import SPDXParsingError + +EXAMPLE_FILE = os.path.join(os.path.dirname(__file__), os.pardir, "data", "example.spdx3.json") + + +def test_parse_file_returns_object_set(): + object_set = parse_file(EXAMPLE_FILE) + + assert isinstance(object_set, spdx_3_0.SHACLObjectSet) + + elements = list(object_set.foreach()) + assert len(elements) == 60 + + relationships = list(object_set.foreach_type("Relationship")) + assert len(relationships) == 28 + + +def test_parse_file_find_by_id(): + object_set = parse_file(EXAMPLE_FILE) + + spdx_id = "http://spdx.org/spdxdocs/examplemaven-0.0.1/enriched-specv3/SPDXRef-gnrtd0" + element = object_set.find_by_id(spdx_id) + + assert element is not None + assert str(element._id) == spdx_id + + +def test_parse_file_unsupported_extension(): + with pytest.raises(SPDXParsingError): + parse_file("some_file.rdf") From 294b44625679dd96da140580a6da301d7b62f00f Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 23 Jun 2026 16:41:23 +0100 Subject: [PATCH 2/6] Temporary skip test on Windows Until official spdx-python-model 0.0.6 release Signed-off-by: Arthit Suriyawongkul --- .github/workflows/install_and_test.yml | 7 ++++ pyproject.toml | 36 +++++++++++++++++-- .../spdx3/parser/json_ld/json_ld_parser.py | 2 +- src/spdx_tools/spdx3/parser/parse_anything.py | 2 ++ tests/spdx3/conftest.py | 11 ++++++ 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/spdx3/conftest.py diff --git a/.github/workflows/install_and_test.yml b/.github/workflows/install_and_test.yml index d661745be..4d419df39 100644 --- a/.github/workflows/install_and_test.yml +++ b/.github/workflows/install_and_test.yml @@ -36,6 +36,13 @@ jobs: python -m pip install tzdata python -m pip install networkx shell: bash + - name: Install SPDX 3 bindings + # The spdx-python-model bindings are built from git and their build-time + # code generation does not currently work on Windows; install them only + # where they build so the SPDX 3 tests run there (and are skipped elsewhere). + if: runner.os != 'Windows' + run: python -m pip install ".[spdx3]" + shell: bash - name: Run tests run: pytest - name: Run CLI diff --git a/pyproject.toml b/pyproject.toml index 791439114..37a12b32e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ dependencies = [ "pyyaml", "rdflib", "semantic_version", - "spdx-python-model @ git+https://github.com/spdx/spdx-python-model.git@main", "uritools", "xmltodict", ] @@ -45,9 +44,15 @@ dynamic = ["version"] [project.optional-dependencies] test = ["pyshacl", "pytest", "tzdata"] -code_style = ["black", "flake8", "isort"] +code_style = ["black", "flake8", "isort", "mypy"] graph_generation = ["networkx", "pygraphviz"] -development = ["black", "flake8", "isort", "networkx", "pyshacl", "pytest"] +# Experimental SPDX 3.0 support is built on the spdx-python-model bindings. +# It is an optional extra (not a core dependency) because the bindings are not +# yet published to PyPI and must be built from git, where the build-time code +# generation does not currently work on Windows. Keeping it optional ensures the +# core package installs on all platforms. Install with: pip install ".[spdx3]" +spdx3 = ["spdx-python-model @ git+https://github.com/spdx/spdx-python-model.git@main"] +development = ["black", "flake8", "isort", "mypy", "networkx", "pyshacl", "pytest"] [project.scripts] pyspdxtools = "spdx_tools.spdx.clitools.pyspdxtools:main" @@ -60,6 +65,31 @@ Repository = "https://github.com/spdx/tools-python.git" Issues = "https://github.com/spdx/tools-python/issues" Changelog = "https://github.com/spdx/tools-python/blob/main/CHANGELOG.md" +[tool.mypy] +python_version = "3.10" +strict = true +mypy_path = "src" +explicit_package_bases = true +# The new SPDX 3 code is fully typed and checked under --strict. +# The legacy SPDX 2 modules are not strict-clean yet and are out of scope here; +# they are excluded below so they don't mask issues in the new code. +files = [ + "src/spdx_tools/spdx3/formats.py", + "src/spdx_tools/spdx3/parser", +] + +# spdx-python-model ships a py.typed marker but its generated bindings expose +# many attributes as Any; this keeps strict checks meaningful without noise. +[[tool.mypy.overrides]] +module = ["spdx_python_model.*"] +ignore_missing_imports = true + +# Pre-existing SPDX 2 code imported by the new modules (e.g. SPDXParsingError) +# is not yet strict-typed; do not fail the new-code check on it. +[[tool.mypy.overrides]] +module = ["spdx_tools.spdx.*"] +ignore_errors = true + [tool.setuptools] include-package-data = true diff --git a/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py b/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py index bc27565c2..cc9203855 100644 --- a/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py +++ b/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py @@ -8,7 +8,7 @@ def parse_from_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectSet: """Read a SPDX 3 JSON-LD file into a SHACLObjectSet (the in-memory representation provided by the spdx-python-model bindings).""" - object_set = spdx_3_0.SHACLObjectSet() + object_set: spdx_3_0.SHACLObjectSet = spdx_3_0.SHACLObjectSet() try: # The binding's deserializer reads from a binary stream. with open(file_name, "rb") as file: diff --git a/src/spdx_tools/spdx3/parser/parse_anything.py b/src/spdx_tools/spdx3/parser/parse_anything.py index 038c467da..e330ac317 100644 --- a/src/spdx_tools/spdx3/parser/parse_anything.py +++ b/src/spdx_tools/spdx3/parser/parse_anything.py @@ -4,6 +4,7 @@ from spdx_tools.spdx3.formats import FileFormat, file_name_to_format from spdx_tools.spdx3.parser.json_ld import json_ld_parser +from spdx_tools.spdx.parser.error import SPDXParsingError def parse_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectSet: @@ -15,3 +16,4 @@ def parse_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectS input_format = file_name_to_format(file_name) if input_format == FileFormat.JSON_LD: return json_ld_parser.parse_from_file(file_name, encoding) + raise SPDXParsingError([f"Unsupported SPDX 3 file format: {input_format}"]) diff --git a/tests/spdx3/conftest.py b/tests/spdx3/conftest.py new file mode 100644 index 000000000..e6804e581 --- /dev/null +++ b/tests/spdx3/conftest.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 + +# The SPDX 3 support depends on the optional `spdx-python-model` bindings, which +# are not installed by default and currently cannot be built from git on Windows. +# When the bindings are unavailable, skip collecting the SPDX 3 test suite instead +# of failing at import time. +try: + import spdx_python_model # noqa: F401 +except ImportError: + collect_ignore_glob = ["*"] From 24069ec1800b7a6a02e136cba42fe067b7b2cb3a Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 23 Jun 2026 17:29:03 +0100 Subject: [PATCH 3/6] Update spdx-python-model to 0.0.6 Signed-off-by: Arthit Suriyawongkul --- .github/workflows/install_and_test.yml | 7 ------- pyproject.toml | 7 +------ src/spdx_tools/spdx3/py.typed | 0 tests/spdx3/conftest.py | 9 --------- tests/spdx3/writer/tag_value/test_write_document.py | 10 +++++++--- 5 files changed, 8 insertions(+), 25 deletions(-) create mode 100644 src/spdx_tools/spdx3/py.typed diff --git a/.github/workflows/install_and_test.yml b/.github/workflows/install_and_test.yml index 4d419df39..d661745be 100644 --- a/.github/workflows/install_and_test.yml +++ b/.github/workflows/install_and_test.yml @@ -36,13 +36,6 @@ jobs: python -m pip install tzdata python -m pip install networkx shell: bash - - name: Install SPDX 3 bindings - # The spdx-python-model bindings are built from git and their build-time - # code generation does not currently work on Windows; install them only - # where they build so the SPDX 3 tests run there (and are skipped elsewhere). - if: runner.os != 'Windows' - run: python -m pip install ".[spdx3]" - shell: bash - name: Run tests run: pytest - name: Run CLI diff --git a/pyproject.toml b/pyproject.toml index 37a12b32e..df0c81643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "pyyaml", "rdflib", "semantic_version", + "spdx-python-model>=0.0.6", "uritools", "xmltodict", ] @@ -46,12 +47,6 @@ dynamic = ["version"] test = ["pyshacl", "pytest", "tzdata"] code_style = ["black", "flake8", "isort", "mypy"] graph_generation = ["networkx", "pygraphviz"] -# Experimental SPDX 3.0 support is built on the spdx-python-model bindings. -# It is an optional extra (not a core dependency) because the bindings are not -# yet published to PyPI and must be built from git, where the build-time code -# generation does not currently work on Windows. Keeping it optional ensures the -# core package installs on all platforms. Install with: pip install ".[spdx3]" -spdx3 = ["spdx-python-model @ git+https://github.com/spdx/spdx-python-model.git@main"] development = ["black", "flake8", "isort", "mypy", "networkx", "pyshacl", "pytest"] [project.scripts] diff --git a/src/spdx_tools/spdx3/py.typed b/src/spdx_tools/spdx3/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/tests/spdx3/conftest.py b/tests/spdx3/conftest.py index e6804e581..2173e3181 100644 --- a/tests/spdx3/conftest.py +++ b/tests/spdx3/conftest.py @@ -1,11 +1,2 @@ # SPDX-FileCopyrightText: 2026-present SPDX contributors # SPDX-License-Identifier: Apache-2.0 - -# The SPDX 3 support depends on the optional `spdx-python-model` bindings, which -# are not installed by default and currently cannot be built from git on Windows. -# When the bindings are unavailable, skip collecting the SPDX 3 test suite instead -# of failing at import time. -try: - import spdx_python_model # noqa: F401 -except ImportError: - collect_ignore_glob = ["*"] diff --git a/tests/spdx3/writer/tag_value/test_write_document.py b/tests/spdx3/writer/tag_value/test_write_document.py index 10d5d54c6..cd1b89156 100644 --- a/tests/spdx3/writer/tag_value/test_write_document.py +++ b/tests/spdx3/writer/tag_value/test_write_document.py @@ -29,7 +29,9 @@ def test_render_creation_info(): output_str = io.StringIO() write_spdx_document(spdx_document, text_output=output_str) - assert output_str.getvalue() == """\ + assert ( + output_str.getvalue() + == """\ ## SPDX Document SPDXID: SPDXRef-FOO name: BAR @@ -38,5 +40,7 @@ def test_render_creation_info(): created: 2024-01-01T00:00:00Z profile: SOFTWARE data license: CC0-1.0 -elements: -""" # noqa: W291 # elements: are printed with a space +elements: \ + +""" # noqa: W291 # elements: are printed with trailing space + ) From 840c7cfcabde8c3b070e8a34bde6add00992d72f Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 23 Jun 2026 21:14:25 +0100 Subject: [PATCH 4/6] Set black target to Python 3.10-3.14 Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 1 + tests/spdx3/writer/tag_value/test_write_document.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index df0c81643..60ffa0441 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ release = "clean --all sdist --formats=gztar bdist_wheel" [tool.black] line-length = 119 +target-version = ["py310", "py311", "py312", "py313", "py314"] include = "(^/src/.*.py|^/tests/.*.py)" [tool.isort] diff --git a/tests/spdx3/writer/tag_value/test_write_document.py b/tests/spdx3/writer/tag_value/test_write_document.py index cd1b89156..495ddf5e0 100644 --- a/tests/spdx3/writer/tag_value/test_write_document.py +++ b/tests/spdx3/writer/tag_value/test_write_document.py @@ -42,5 +42,5 @@ def test_render_creation_info(): data license: CC0-1.0 elements: \ -""" # noqa: W291 # elements: are printed with trailing space - ) +""" + ) # noqa: W291 # elements: are printed with trailing space From 5eb1641c6ea8b7d5e482417ec238b29ef267e25a Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 23 Jun 2026 21:29:30 +0100 Subject: [PATCH 5/6] Delete test_write_document.py SDPX 3 does not have tag:value format Signed-off-by: Arthit Suriyawongkul --- .../writer/tag_value/test_write_document.py | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 tests/spdx3/writer/tag_value/test_write_document.py diff --git a/tests/spdx3/writer/tag_value/test_write_document.py b/tests/spdx3/writer/tag_value/test_write_document.py deleted file mode 100644 index 495ddf5e0..000000000 --- a/tests/spdx3/writer/tag_value/test_write_document.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2024 spdx contributors -# -# SPDX-License-Identifier: Apache-2.0 -import io -from datetime import datetime - -from semantic_version import Version - -from spdx_tools.spdx3.model import CreationInfo, ProfileIdentifierType, SpdxDocument -from spdx_tools.spdx3.writer.console.spdx_document_writer import write_spdx_document - - -def test_render_creation_info(): - fake_datetime = datetime(year=2024, month=1, day=1) - spec_version = Version("3.0.0") - creation_info = CreationInfo( - spec_version=spec_version, - created=fake_datetime, - created_by=[], - profile=[ProfileIdentifierType.SOFTWARE], - ) - spdx_document = SpdxDocument( - spdx_id="SPDXRef-FOO", - name="BAR", - element=[], - root_element=[], - creation_info=creation_info, - ) - output_str = io.StringIO() - write_spdx_document(spdx_document, text_output=output_str) - - assert ( - output_str.getvalue() - == """\ -## SPDX Document -SPDXID: SPDXRef-FOO -name: BAR -# Creation Information - specVersion: 3.0.0 - created: 2024-01-01T00:00:00Z - profile: SOFTWARE - data license: CC0-1.0 -elements: \ - -""" - ) # noqa: W291 # elements: are printed with trailing space From c476ca92174d4c4a0a96d77546e98c0d40e9d9d0 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 23 Jun 2026 22:34:06 +0100 Subject: [PATCH 6/6] Use Protocol Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 1 + src/spdx_tools/spdx3/object_set.py | 22 +++++++++++++++++++ .../spdx3/parser/json_ld/json_ld_parser.py | 5 +++-- src/spdx_tools/spdx3/parser/parse_anything.py | 5 ++--- tests/spdx3/parser/test_read.py | 4 ++-- 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/spdx_tools/spdx3/object_set.py diff --git a/pyproject.toml b/pyproject.toml index 60ffa0441..3035e717b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ explicit_package_bases = true # they are excluded below so they don't mask issues in the new code. files = [ "src/spdx_tools/spdx3/formats.py", + "src/spdx_tools/spdx3/object_set.py", "src/spdx_tools/spdx3/parser", ] diff --git a/src/spdx_tools/spdx3/object_set.py b/src/spdx_tools/spdx3/object_set.py new file mode 100644 index 000000000..41cb13e71 --- /dev/null +++ b/src/spdx_tools/spdx3/object_set.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2026-present SPDX contributors +# SPDX-License-Identifier: Apache-2.0 +from typing import Any, Iterable, Optional, Protocol, Set, runtime_checkable + + +@runtime_checkable +class SpdxObjectSet(Protocol): + """Version-agnostic structural type for a SHACL object set. + + Satisfied by ``SHACLObjectSet`` from any ``spdx_python_model.vX_Y_Z`` + version module, so callers are not tied to a specific SPDX spec version. + """ + + def foreach(self) -> Iterable[Any]: ... + + def foreach_type(self, typ: Any, *, match_subclass: bool = True) -> Iterable[Any]: ... + + def find_by_id(self, _id: str, default: Optional[Any] = None) -> Optional[Any]: ... + + def add(self, obj: Any) -> Any: ... + + def link(self) -> Set[str]: ... diff --git a/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py b/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py index cc9203855..2afe74303 100644 --- a/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py +++ b/src/spdx_tools/spdx3/parser/json_ld/json_ld_parser.py @@ -2,13 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 from spdx_python_model import v3_0_1 as spdx_3_0 +from spdx_tools.spdx3.object_set import SpdxObjectSet from spdx_tools.spdx.parser.error import SPDXParsingError -def parse_from_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectSet: +def parse_from_file(file_name: str, encoding: str = "utf-8") -> SpdxObjectSet: """Read a SPDX 3 JSON-LD file into a SHACLObjectSet (the in-memory representation provided by the spdx-python-model bindings).""" - object_set: spdx_3_0.SHACLObjectSet = spdx_3_0.SHACLObjectSet() + object_set = spdx_3_0.SHACLObjectSet() try: # The binding's deserializer reads from a binary stream. with open(file_name, "rb") as file: diff --git a/src/spdx_tools/spdx3/parser/parse_anything.py b/src/spdx_tools/spdx3/parser/parse_anything.py index e330ac317..58fd3bc14 100644 --- a/src/spdx_tools/spdx3/parser/parse_anything.py +++ b/src/spdx_tools/spdx3/parser/parse_anything.py @@ -1,13 +1,12 @@ # SPDX-FileCopyrightText: 2026-present SPDX contributors # SPDX-License-Identifier: Apache-2.0 -from spdx_python_model import v3_0_1 as spdx_3_0 - from spdx_tools.spdx3.formats import FileFormat, file_name_to_format +from spdx_tools.spdx3.object_set import SpdxObjectSet from spdx_tools.spdx3.parser.json_ld import json_ld_parser from spdx_tools.spdx.parser.error import SPDXParsingError -def parse_file(file_name: str, encoding: str = "utf-8") -> spdx_3_0.SHACLObjectSet: +def parse_file(file_name: str, encoding: str = "utf-8") -> SpdxObjectSet: """Parse a SPDX 3 file into a SHACLObjectSet, dispatching on the file format. SPDX 3.0 currently defines a single serialization (JSON-LD); the dispatch is diff --git a/tests/spdx3/parser/test_read.py b/tests/spdx3/parser/test_read.py index 1ebf8c226..f2238a588 100644 --- a/tests/spdx3/parser/test_read.py +++ b/tests/spdx3/parser/test_read.py @@ -3,8 +3,8 @@ import os import pytest -from spdx_python_model import v3_0_1 as spdx_3_0 +from spdx_tools.spdx3.object_set import SpdxObjectSet from spdx_tools.spdx3.parser import parse_file from spdx_tools.spdx.parser.error import SPDXParsingError @@ -14,7 +14,7 @@ def test_parse_file_returns_object_set(): object_set = parse_file(EXAMPLE_FILE) - assert isinstance(object_set, spdx_3_0.SHACLObjectSet) + assert isinstance(object_set, SpdxObjectSet) elements = list(object_set.foreach()) assert len(elements) == 60