Skip to content
Merged
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
17 changes: 10 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ build-backend = "poetry.core.masonry.api"
name = "pyshacl"
# Don't forget to change the version number in __init__.py, Dockerfile, and CITATION.cff along with this one
version = "0.31.0"
# Ruff and Poetry both now read target-version from [project.requires-python]
# The <4 is reauired for compatiblity with OWL-RL that requdires Python <4
requires-python = ">=3.9,<4"
requires-python = ">=3.9"
description = "Python SHACL Validator"
license = { file = "LICENSE.txt" }
authors = [
Expand Down Expand Up @@ -46,8 +44,8 @@ classifiers = [
"Operating System :: OS Independent"
]
dependencies = [
"rdflib[html]>=7.1.1,<8.0,!=7.1.2",
"owlrl>=7.1.2,<8",
"rdflib[html]>=7.3.0,<8.0",
"owlrl>=7.6.2,<8",
"prettytable>=3.5.0; python_version<'3.12'",
"prettytable>=3.7.0; python_version>='3.12'",
"packaging>=21.3",
Expand All @@ -64,7 +62,11 @@ changelog = "https://github.com/RDFLib/pySHACL/blob/master/CHANGELOG.md"
[project.optional-dependencies]
# These are equivelent to python package "extras"
js = [
"pyduktape2<1,>=0.4.6"
"pyduktape2<1,>=0.4.6; python_version<='3.13'",
"pyduktape2<1,>=0.5.0; python_version>='3.13'",
]
oxigraph = [
"pyoxigraph>=0.5.6"
]
http = [
"sanic<23,>=22.12",
Expand Down Expand Up @@ -191,7 +193,7 @@ testpaths = [
legacy_tox_ini = """
[tox]
skipsdist = true
envlist = py39, py310, py311, py312, lint, type-checking
envlist = py39, py310, py311, py312, py313, lint, type-checking
toxworkdir={env:TOX_WORK_DIR:.tox}

[testenv]
Expand Down Expand Up @@ -241,6 +243,7 @@ commands =
[gh]
# Don't include lint or type-checking in gh-actions matrix
python =
3.13 = py313
3.12 = py312
3.11 = py311
3.10 = py310
Expand Down
2 changes: 2 additions & 0 deletions pyshacl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ def main(prog: Union[str, None] = None) -> None:
validator_kwargs['allow_infos'] = True
if args.allow_warnings:
validator_kwargs['allow_warnings'] = True
if args.max_depth is not None:
validator_kwargs['max_validation_depth'] = args.max_depth
if args.shacl_file_format:
_f: str = args.shacl_file_format
if _f != "auto":
Expand Down
4 changes: 2 additions & 2 deletions pyshacl/constraints/constraint_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def make_v_result_description(
):
"""
:param datagraph:
:type datagraph: rdflib.Graph | rdflib.ConjunctiveGraph | rdflib.Dataset
:type datagraph: rdflib.Graph | rdflib.Dataset
:param focus_node:
:type focus_node: RDFNode
:param severity:
Expand Down Expand Up @@ -231,7 +231,7 @@ def make_v_result(
):
"""
:param datagraph:
:type datagraph: rdflib.Graph | rdflib.ConjunctiveGraph | rdflib.Dataset
:type datagraph: rdflib.Graph | rdflib.Dataset
:param focus_node:
:type focus_node: RDFNode
:param value_node:
Expand Down
70 changes: 35 additions & 35 deletions pyshacl/entrypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,29 @@
from sys import stderr
from typing import Dict, List, Optional, Tuple, Union

from rdflib import ConjunctiveGraph, Dataset, Graph, Literal, URIRef
from rdflib import Dataset, Graph, Literal, URIRef

from pyshacl.errors import ReportableRuntimeError, ValidationFailure
from pyshacl.pytypes import GraphLike

from .consts import SH, RDF_type
from .graph_abstraction import DataGraph, has_oxigraph, ox_Store
from .monkey import apply_patches, rdflib_bool_patch, rdflib_bool_unpatch
from .rdfutil import load_from_source
from .rule_expand_runner import RuleExpandRunner
from .validator import Validator, assign_baked_in
from .validator_conformance import check_dash_result

DataGraphInput = Union[GraphLike, BufferedIOBase, TextIOBase, str, bytes]
DataGraphInput = Union[DataGraph, GraphLike, BufferedIOBase, TextIOBase, str, bytes]
MultiDataGraphInput = Sequence[DataGraphInput]


def _is_multi_data_graph_input(data_graph: object) -> bool:
if isinstance(data_graph, (str, bytes, BufferedIOBase, TextIOBase, Graph, Dataset, ConjunctiveGraph)):
if isinstance(data_graph, (str, bytes, DataGraph, BufferedIOBase, TextIOBase, Graph, Dataset)):
return False
return isinstance(data_graph, (tuple, list, set, frozenset, Sequence))


def _multi_data_graph_key(source: DataGraphInput) -> Union[str, URIRef]:
if isinstance(source, (Graph, Dataset, ConjunctiveGraph)):
return source.identifier
if isinstance(source, (BufferedIOBase, TextIOBase)):
return getattr(source, "name", repr(source))
if isinstance(source, bytes):
try:
return source.decode("utf-8")
except UnicodeDecodeError:
return repr(source)
return str(source)


def validate(
data_graph: Union[DataGraphInput, MultiDataGraphInput],
*args,
Expand Down Expand Up @@ -127,12 +115,12 @@ def validate(
sparql_mode=sparql_mode,
focus_nodes=focus_nodes,
use_shapes=use_shapes,
multi_data_graphs_mode=resolved_mode,
**kwargs,
)
if len(data_graphs) == 1:
data_graph = data_graphs[0]
else:
# Combined mode, load all the sources into a single dataset
data_graph_format = kwargs.get('data_graph_format', None)
combined_dataset = Dataset(default_union=True)
for source in data_graphs:
Expand All @@ -145,7 +133,7 @@ def validate(
logger=log,
)
data_graph = combined_dataset
do_check_dash_result = kwargs.pop('check_dash_result', False) # type: bool
do_check_dash_result: bool = kwargs.pop('check_dash_result', False)
if kwargs.get('meta_shacl', False):
to_meta_val = shacl_graph or data_graph
conforms, v_r, v_t = meta_validate(to_meta_val, inference=inference, **kwargs)
Expand Down Expand Up @@ -173,8 +161,6 @@ def validate(
raise ReportableRuntimeError("Cannot use SPARQL Remote Graph Mode with extra Ontology Graph inoculation.")
if isinstance(data_graph, bytes):
data_graph = data_graph.decode('utf-8')
else:
data_graph = data_graph
ephemeral = False
inplace = True
if (
Expand All @@ -195,11 +181,19 @@ def validate(
auth = None
store = SPARQLStore(query_endpoint=query_endpoint, auth=auth, method=method)
loaded_dg = Dataset(store=store, default_union=True)
dg = DataGraph.from_rdflib_dataset(loaded_dg)
elif isinstance(data_graph, DataGraph):
loaded_dg = data_graph.impl
dg = data_graph
elif has_oxigraph and isinstance(data_graph, ox_Store):
loaded_dg = data_graph
dg = DataGraph.from_oxigraph_store(loaded_dg)
else:
# force no owl imports on data_graph
loaded_dg = load_from_source(
data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log
)
dg = DataGraph.from_rdflib(loaded_dg)
ont_graph_format = kwargs.pop('ont_graph_format', None)
if ont_graph is not None:
loaded_og = load_from_source(
Expand Down Expand Up @@ -241,7 +235,7 @@ def validate(
validator = None
try:
validator = Validator(
loaded_dg,
dg,
shacl_graph=loaded_sg,
ont_graph=loaded_og,
options=validator_options_dict,
Expand All @@ -252,7 +246,7 @@ def validate(
report_graph = e
report_text = "Validation Failure - {}".format(e.message)
if do_check_dash_result and validator is not None:
passes = check_dash_result(validator, report_graph, loaded_sg or loaded_dg)
passes = check_dash_result(validator, report_graph, loaded_sg or dg)
return passes, report_graph, report_text
do_serialize_report_graph = kwargs.pop('serialize_report_graph', False)
if do_serialize_report_graph and isinstance(report_graph, Graph):
Expand All @@ -277,24 +271,23 @@ def validate_each(
sparql_mode: Optional[bool] = False,
focus_nodes: Optional[List[Union[str, URIRef]]] = None,
use_shapes: Optional[List[Union[str, URIRef]]] = None,
multi_data_graphs_mode: Optional[str] = None,
**kwargs,
) -> Dict[Union[str, URIRef], Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]]:
) -> Dict[int, Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]]:
"""
:param data_graphs: Sequence of data graphs or sources to validate independently
:type data_graphs: Sequence
:param multi_data_graphs_mode: Optional mode hint for compatibility with validate()
:type multi_data_graphs_mode: str | None
:return: dict mapping each input graph identifier to its validation results
:return: dict mapping each input graph index to its validation results
"""

if not _is_multi_data_graph_input(data_graphs):
raise ReportableRuntimeError("validate_each expects a sequence of data graphs to validate.")
data_graph_list = list(data_graphs)
if len(data_graph_list) < 1:
raise ReportableRuntimeError("No data graphs were provided for validate_each.")
results: Dict[Union[str, URIRef], Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]] = {}
for data_graph in data_graph_list:
results: Dict[int, Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]] = {}
for datagraph_i, data_graph in enumerate(data_graph_list):
result = validate(
data_graph,
*args,
Expand All @@ -310,10 +303,9 @@ def validate_each(
sparql_mode=sparql_mode,
focus_nodes=focus_nodes,
use_shapes=use_shapes,
multi_data_graphs_mode=multi_data_graphs_mode,
**kwargs,
)
results[_multi_data_graph_key(data_graph)] = result
results[datagraph_i] = result
return results


Expand Down Expand Up @@ -416,10 +408,18 @@ def shacl_rules(
else:
ephemeral = False
use_js = kwargs.pop('js', None)
# force no owl imports on data_graph
loaded_dg = load_from_source(
data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log
)
if isinstance(data_graph, DataGraph):
loaded_dg = data_graph.impl
dg = data_graph
elif has_oxigraph and isinstance(data_graph, ox_Store):
loaded_dg = data_graph
dg = DataGraph.from_oxigraph_store(loaded_dg)
else:
# force no owl imports on data_graph
loaded_dg = load_from_source(
data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log
)
dg = DataGraph.from_rdflib(loaded_dg)
ont_graph_format = kwargs.pop('ont_graph_format', None)
if ont_graph is not None:
loaded_og = load_from_source(
Expand Down Expand Up @@ -450,7 +450,7 @@ def shacl_rules(
serialize_expanded_graph = kwargs.get('serialize_expanded_graph', None)
try:
runner = RuleExpandRunner(
loaded_dg,
dg,
shacl_graph=loaded_sg,
ont_graph=loaded_og,
options=runner_options_dict,
Expand All @@ -466,7 +466,7 @@ def shacl_rules(
g.add((URIRef("<urn:rdflib:pyshacl:shacl-rules-error>"), SH.message, Literal(error)))
return g
if serialize_expanded_graph:
guess_format = "trig" if isinstance(expanded_graph, (Dataset, ConjunctiveGraph)) else "turtle"
guess_format = "trig" if isinstance(expanded_graph, Dataset) else "turtle"
serialize_format = kwargs.get('serialize_expanded_graph_format', guess_format)
return expanded_graph.serialize(format=serialize_format)
return expanded_graph
79 changes: 70 additions & 9 deletions pyshacl/extras/js/function.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
#
#
from __future__ import annotations

import functools
import typing
from typing import Dict, Union

from rdflib.plugins.sparql.operators import register_custom_function, unregister_custom_function
from rdflib.plugins.sparql.sparql import SPARQLError

from pyshacl.errors import ReportableRuntimeError
from pyshacl.functions.shacl_function import SHACLFunction

from ...errors import ReportableRuntimeError
from ...functions.shacl_function import SHACLFunction
from ...graph_abstraction import has_oxigraph, to_ox, to_rdf
from .js_executable import JSExecutable

if typing.TYPE_CHECKING:
from pyshacl.shapes_graph import ShapesGraph
from ...graph_abstraction import DataGraph
from ...shapes_graph import ShapesGraph
if has_oxigraph:
from pyoxigraph import BlankNode as ox_BlankNode
from pyoxigraph import NamedNode as ox_NamedNode
from pyoxigraph import Triple as ox_Triple
else:
ox_BlankNode = None
ox_NamedNode = None
ox_Triple = None


class JSFunction(SHACLFunction):
Expand Down Expand Up @@ -56,10 +68,59 @@ def execute_from_sparql(self, e, ctx):
res = results['_result']
return res

def apply(self, g):
def execute_oxigraph(
self,
g: 'DataGraph',
args_map: Dict[str, Union[ox_NamedNode, ox_BlankNode, ox_Triple]],
):
"""Run the SHACL-JS function body and return a pyoxigraph term."""
rdf_args_map = {k: to_rdf(v) if v is not None else None for k, v in args_map.items()}
results = self.js_exe.execute(g, rdf_args_map, mode="function", return_type=self.rtype)
res = results['_result']
if res is None:
return None
return to_ox(res)

def execute_from_sparql_oxigraph(
self,
g: 'DataGraph',
*args: Union[ox_NamedNode, ox_BlankNode, ox_Triple],
):
"""Oxigraph custom-function callback: evaluated argument terms in, one RDF term out."""
if not g.is_oxigraph:
raise ReportableRuntimeError("execute_from_sparql_oxigraph requires an Oxigraph-backed DataGraph.")
params = self.get_params_in_order()
num_params = len(params)
num_args = len(args)
if num_args > num_params:
raise ValueError("Too many parameters passed to JSFunction {}.".format(self.node))
if num_args < num_params:
raise ValueError("Too few parameters passed to JSFunction {}.".format(self.node))
args_map: Dict[str, Union[ox_NamedNode, ox_BlankNode, ox_Triple]] = {}
for i, p in enumerate(params):
ox_arg = args[i]
ln = p.localname
if ox_arg is None and p.optional is False:
raise ReportableRuntimeError("Got NoneType for Non-optional argument {}.".format(ln))
args_map[ln] = ox_arg
return self.execute_oxigraph(g, args_map)

def apply(self, g: 'DataGraph'):
super(JSFunction, self).apply(g)
register_custom_function(self.node, self.execute_from_sparql, True, True)
if has_oxigraph:
g.register_custom_function(
self.node,
self.execute_from_sparql,
functools.partial(self.execute_from_sparql_oxigraph, g),
True,
True,
)
else:
g.register_custom_function(self.node, self.execute_from_sparql, None, True, True)

def unapply(self, g):
def unapply(self, g: 'DataGraph'):
super(JSFunction, self).unapply(g)
unregister_custom_function(self.node, self.execute_from_sparql)
if has_oxigraph:
g.unregister_custom_function(self.node, self.execute_from_sparql, self.execute_from_sparql_oxigraph)
else:
g.unregister_custom_function(self.node, self.execute_from_sparql, None)
4 changes: 2 additions & 2 deletions pyshacl/extras/js/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ def apply(
if this_added:
added += 1
if added > 0:
if isinstance(data_graph, (rdflib.Dataset, rdflib.ConjunctiveGraph)):
if isinstance(data_graph, rdflib.Dataset):
if target_graph_identifier is not None:
target_graph = data_graph.get_context(target_graph_identifier)
else:
target_graph = data_graph.default_context
target_graph = data_graph.default_graph
else:
target_graph = data_graph
for s in sets_to_add:
Expand Down
Loading
Loading