Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/spdx3_validate/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
from .main import main # noqa: F401
# SPDX-FileCopyrightText: Copyright (c) 2024 Joshua Watt
# SPDX-License-Identifier: MIT

"""Initialization for spdx3_validate package."""

from .main import main

__all__ = ["main"]
3 changes: 3 additions & 0 deletions src/spdx3_validate/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# Copyright (c) 2024 Joshua Watt
#
# SPDX-License-Identifier: MIT

"""Main entry point for SPDX 3 validation tool."""

from .main import main

main()
51 changes: 29 additions & 22 deletions src/spdx3_validate/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,41 @@
#
# SPDX-License-Identifier: MIT

"""Main module for SPDX 3 validation tool."""

import argparse
import halo
import json
import urllib.request
import sys
import textwrap

from pathlib import Path

import halo
import jsonschema
import pyshacl
import rdflib
import sys
import textwrap
import urllib.request

from rdflib import RDF, RDFS, SH, URIRef

from pathlib import Path
from .version import VERSION
from .spdx_versions import find_version, SPDX_VERSIONS


def read_location(location):
"""Read a file from a location."""
if "://" in location:
with urllib.request.urlopen(location) as f:
return f.read()
elif location == "-":
return sys.stdin.read()
else:
with Path(location).open("r") as f:
with Path(location).open("r", encoding="utf-8") as f:
return f.read()


def derives_from(cls, target, shacl_graph):
"""Check if a class derives from another class."""
if cls == target:
return True

Expand All @@ -42,6 +48,7 @@ def derives_from(cls, target, shacl_graph):


def check_graph(graph, shacl_graph, current_version, error_external):
"""Check a graph against SHACL shapes."""
errors = []

conforms, results, _ = pyshacl.validate(
Expand Down Expand Up @@ -77,11 +84,6 @@ def pnode(n):
external_spdxids.add(str(spdxid))

def check_external_ref_error(r):
nonlocal results
nonlocal shacl_graph
nonlocal graph
nonlocal external_spdxids

if (r, RDF.type, SH.ValidationResult) not in results:
return False

Expand Down Expand Up @@ -147,13 +149,16 @@ def check_external_ref_error(r):


def iter_validation_errors(err):
"""Iterate over all validation errors."""
if err.context:
for e in err.context:
yield e
yield from iter_validation_errors(e)


def print_schema_error(err, filename, indent=0):
"""Print a JSON Schema validation error."""

def print_err(e, indent, fn=None, message=False):
loc = e.json_path
if fn:
Expand Down Expand Up @@ -194,6 +199,7 @@ def print_err(e, indent, fn=None, message=False):


def main(cmdline_args=None):
"""Main entry point for SPDX 3 validation tool."""
parser = argparse.ArgumentParser(
description=f"Validate SPDX 3 files Version {VERSION}"
)
Expand Down Expand Up @@ -264,7 +270,8 @@ def main(cmdline_args=None):
elif current_version != version:
spinner.fail()
print(
f"{j} has incompatible version {version.pretty}. Other documents are {current_version.pretty}"
f"{j} has incompatible version {version.pretty}. "
f"Other documents are {current_version.pretty}"
)
return 1

Expand Down Expand Up @@ -311,20 +318,20 @@ def main(cmdline_args=None):

if json_errors:
print(f"ERROR: JSON Schema validation failed for {fn}:")
for e in json_errors:
print_schema_error(e, fn)
for err in json_errors:
print_schema_error(err, fn)
errors += 1

with halo.Halo(f"Checking SHACL for {fn}", enabled=not args.quiet) as spinner:
e = check_graph(g, shacl_graph, current_version, True)
if e:
err = check_graph(g, shacl_graph, current_version, True)
if err:
spinner.fail()
else:
spinner.succeed()

if e:
if err:
print(f"ERROR: SHACL Validation failed for {fn}:")
print("\n".join(e))
print("\n".join(err))
errors += 1

if len(files) > 1 and args.check_merged:
Expand All @@ -334,15 +341,15 @@ def main(cmdline_args=None):
for _, _, g in files:
merged_g += g

e = check_graph(g, shacl_graph, current_version, False)
if e:
err = check_graph(g, shacl_graph, current_version, False)
if err:
spinner.fail()
else:
spinner.succeed()

if e:
if err:
print("ERROR: SHACL Validation failed on merged files:")
print("\n".join(e))
print("\n".join(err))
errors += 1
else:
print(
Expand Down
20 changes: 13 additions & 7 deletions src/spdx3_validate/spdx_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,38 @@
#
# SPDX-License-Identifier: MIT

"""SPDX versions and related utilities."""

from collections import namedtuple

from rdflib import RDF, URIRef


SpdxVersion = namedtuple(
"SpdxVersion",
["context_url", "shacl_url", "schema_url", "pretty", "rdf_base", "get_imports"],
)


def get_3_0_0_imports(graph):
RDF_BASE = URIRef("https://spdx.org/rdf/3.0.0/terms/")
"""Get imported SPDX IDs from an SPDX 3.0.0 graph."""
RDF_BASE = URIRef(
"https://spdx.org/rdf/3.0.0/terms/"
) # pylint: disable=invalid-name

for doc in graph.subjects(RDF.type, RDF_BASE + "Core/SpdxDocument"):
for i in graph.objects(doc, RDF_BASE + "Core/imports"):
for spdxid in graph.objects(i, RDF_BASE + "Core/externalSpdxId"):
yield spdxid
yield from graph.objects(i, RDF_BASE + "Core/externalSpdxId")


def get_3_0_1_imports(graph):
RDF_BASE = URIRef("https://spdx.org/rdf/3.0.1/terms/")
"""Get imported SPDX IDs from an SPDX 3.0.1 graph."""
RDF_BASE = URIRef(
"https://spdx.org/rdf/3.0.1/terms/"
) # pylint: disable=invalid-name

for doc in graph.subjects(RDF.type, RDF_BASE + "Core/SpdxDocument"):
for i in graph.objects(doc, RDF_BASE + "Core/import"):
for spdxid in graph.objects(i, RDF_BASE + "Core/externalSpdxId"):
yield spdxid
yield from graph.objects(i, RDF_BASE + "Core/externalSpdxId")


SPDX_VERSIONS = (
Expand All @@ -52,6 +57,7 @@ def get_3_0_1_imports(graph):


def find_version(context_url):
"""Find an SPDX version by its context URL."""
for s in SPDX_VERSIONS:
if s.context_url == context_url:
return s
Expand Down
5 changes: 5 additions & 0 deletions src/spdx3_validate/version.py
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 Joshua Watt
# SPDX-License-Identifier: MIT

"""Library version information."""

VERSION = "0.0.5"