Skip to content

Commit 68a76a3

Browse files
authored
Merge pull request #324 from RDFLib/big_updates
Merge big_updates into Main.
2 parents 68e07f8 + da039d8 commit 68a76a3

32 files changed

Lines changed: 2287 additions & 297 deletions

pyproject.toml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ build-backend = "poetry.core.masonry.api"
66
name = "pyshacl"
77
# Don't forget to change the version number in __init__.py, Dockerfile, and CITATION.cff along with this one
88
version = "0.31.0"
9-
# Ruff and Poetry both now read target-version from [project.requires-python]
10-
# The <4 is reauired for compatiblity with OWL-RL that requdires Python <4
11-
requires-python = ">=3.9,<4"
9+
requires-python = ">=3.9"
1210
description = "Python SHACL Validator"
1311
license = { file = "LICENSE.txt" }
1412
authors = [
@@ -46,8 +44,8 @@ classifiers = [
4644
"Operating System :: OS Independent"
4745
]
4846
dependencies = [
49-
"rdflib[html]>=7.1.1,<8.0,!=7.1.2",
50-
"owlrl>=7.1.2,<8",
47+
"rdflib[html]>=7.3.0,<8.0",
48+
"owlrl>=7.6.2,<8",
5149
"prettytable>=3.5.0; python_version<'3.12'",
5250
"prettytable>=3.7.0; python_version>='3.12'",
5351
"packaging>=21.3",
@@ -64,7 +62,11 @@ changelog = "https://github.com/RDFLib/pySHACL/blob/master/CHANGELOG.md"
6462
[project.optional-dependencies]
6563
# These are equivelent to python package "extras"
6664
js = [
67-
"pyduktape2<1,>=0.4.6"
65+
"pyduktape2<1,>=0.4.6; python_version<='3.13'",
66+
"pyduktape2<1,>=0.5.0; python_version>='3.13'",
67+
]
68+
oxigraph = [
69+
"pyoxigraph>=0.5.6"
6870
]
6971
http = [
7072
"sanic<23,>=22.12",
@@ -191,7 +193,7 @@ testpaths = [
191193
legacy_tox_ini = """
192194
[tox]
193195
skipsdist = true
194-
envlist = py39, py310, py311, py312, lint, type-checking
196+
envlist = py39, py310, py311, py312, py313, lint, type-checking
195197
toxworkdir={env:TOX_WORK_DIR:.tox}
196198
197199
[testenv]
@@ -241,6 +243,7 @@ commands =
241243
[gh]
242244
# Don't include lint or type-checking in gh-actions matrix
243245
python =
246+
3.13 = py313
244247
3.12 = py312
245248
3.11 = py311
246249
3.10 = py310

pyshacl/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ def main(prog: Union[str, None] = None) -> None:
322322
validator_kwargs['allow_infos'] = True
323323
if args.allow_warnings:
324324
validator_kwargs['allow_warnings'] = True
325+
if args.max_depth is not None:
326+
validator_kwargs['max_validation_depth'] = args.max_depth
325327
if args.shacl_file_format:
326328
_f: str = args.shacl_file_format
327329
if _f != "auto":

pyshacl/constraints/constraint_component.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def make_v_result_description(
141141
):
142142
"""
143143
:param datagraph:
144-
:type datagraph: rdflib.Graph | rdflib.ConjunctiveGraph | rdflib.Dataset
144+
:type datagraph: rdflib.Graph | rdflib.Dataset
145145
:param focus_node:
146146
:type focus_node: RDFNode
147147
:param severity:
@@ -231,7 +231,7 @@ def make_v_result(
231231
):
232232
"""
233233
:param datagraph:
234-
:type datagraph: rdflib.Graph | rdflib.ConjunctiveGraph | rdflib.Dataset
234+
:type datagraph: rdflib.Graph | rdflib.Dataset
235235
:param focus_node:
236236
:type focus_node: RDFNode
237237
:param value_node:

pyshacl/entrypoints.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,29 @@
77
from sys import stderr
88
from typing import Dict, List, Optional, Tuple, Union
99

10-
from rdflib import ConjunctiveGraph, Dataset, Graph, Literal, URIRef
10+
from rdflib import Dataset, Graph, Literal, URIRef
1111

1212
from pyshacl.errors import ReportableRuntimeError, ValidationFailure
1313
from pyshacl.pytypes import GraphLike
1414

1515
from .consts import SH, RDF_type
16+
from .graph_abstraction import DataGraph, has_oxigraph, ox_Store
1617
from .monkey import apply_patches, rdflib_bool_patch, rdflib_bool_unpatch
1718
from .rdfutil import load_from_source
1819
from .rule_expand_runner import RuleExpandRunner
1920
from .validator import Validator, assign_baked_in
2021
from .validator_conformance import check_dash_result
2122

22-
DataGraphInput = Union[GraphLike, BufferedIOBase, TextIOBase, str, bytes]
23+
DataGraphInput = Union[DataGraph, GraphLike, BufferedIOBase, TextIOBase, str, bytes]
2324
MultiDataGraphInput = Sequence[DataGraphInput]
2425

2526

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

3132

32-
def _multi_data_graph_key(source: DataGraphInput) -> Union[str, URIRef]:
33-
if isinstance(source, (Graph, Dataset, ConjunctiveGraph)):
34-
return source.identifier
35-
if isinstance(source, (BufferedIOBase, TextIOBase)):
36-
return getattr(source, "name", repr(source))
37-
if isinstance(source, bytes):
38-
try:
39-
return source.decode("utf-8")
40-
except UnicodeDecodeError:
41-
return repr(source)
42-
return str(source)
43-
44-
4533
def validate(
4634
data_graph: Union[DataGraphInput, MultiDataGraphInput],
4735
*args,
@@ -127,12 +115,12 @@ def validate(
127115
sparql_mode=sparql_mode,
128116
focus_nodes=focus_nodes,
129117
use_shapes=use_shapes,
130-
multi_data_graphs_mode=resolved_mode,
131118
**kwargs,
132119
)
133120
if len(data_graphs) == 1:
134121
data_graph = data_graphs[0]
135122
else:
123+
# Combined mode, load all the sources into a single dataset
136124
data_graph_format = kwargs.get('data_graph_format', None)
137125
combined_dataset = Dataset(default_union=True)
138126
for source in data_graphs:
@@ -145,7 +133,7 @@ def validate(
145133
logger=log,
146134
)
147135
data_graph = combined_dataset
148-
do_check_dash_result = kwargs.pop('check_dash_result', False) # type: bool
136+
do_check_dash_result: bool = kwargs.pop('check_dash_result', False)
149137
if kwargs.get('meta_shacl', False):
150138
to_meta_val = shacl_graph or data_graph
151139
conforms, v_r, v_t = meta_validate(to_meta_val, inference=inference, **kwargs)
@@ -173,8 +161,6 @@ def validate(
173161
raise ReportableRuntimeError("Cannot use SPARQL Remote Graph Mode with extra Ontology Graph inoculation.")
174162
if isinstance(data_graph, bytes):
175163
data_graph = data_graph.decode('utf-8')
176-
else:
177-
data_graph = data_graph
178164
ephemeral = False
179165
inplace = True
180166
if (
@@ -195,11 +181,19 @@ def validate(
195181
auth = None
196182
store = SPARQLStore(query_endpoint=query_endpoint, auth=auth, method=method)
197183
loaded_dg = Dataset(store=store, default_union=True)
184+
dg = DataGraph.from_rdflib_dataset(loaded_dg)
185+
elif isinstance(data_graph, DataGraph):
186+
loaded_dg = data_graph.impl
187+
dg = data_graph
188+
elif has_oxigraph and isinstance(data_graph, ox_Store):
189+
loaded_dg = data_graph
190+
dg = DataGraph.from_oxigraph_store(loaded_dg)
198191
else:
199192
# force no owl imports on data_graph
200193
loaded_dg = load_from_source(
201194
data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log
202195
)
196+
dg = DataGraph.from_rdflib(loaded_dg)
203197
ont_graph_format = kwargs.pop('ont_graph_format', None)
204198
if ont_graph is not None:
205199
loaded_og = load_from_source(
@@ -241,7 +235,7 @@ def validate(
241235
validator = None
242236
try:
243237
validator = Validator(
244-
loaded_dg,
238+
dg,
245239
shacl_graph=loaded_sg,
246240
ont_graph=loaded_og,
247241
options=validator_options_dict,
@@ -252,7 +246,7 @@ def validate(
252246
report_graph = e
253247
report_text = "Validation Failure - {}".format(e.message)
254248
if do_check_dash_result and validator is not None:
255-
passes = check_dash_result(validator, report_graph, loaded_sg or loaded_dg)
249+
passes = check_dash_result(validator, report_graph, loaded_sg or dg)
256250
return passes, report_graph, report_text
257251
do_serialize_report_graph = kwargs.pop('serialize_report_graph', False)
258252
if do_serialize_report_graph and isinstance(report_graph, Graph):
@@ -277,24 +271,23 @@ def validate_each(
277271
sparql_mode: Optional[bool] = False,
278272
focus_nodes: Optional[List[Union[str, URIRef]]] = None,
279273
use_shapes: Optional[List[Union[str, URIRef]]] = None,
280-
multi_data_graphs_mode: Optional[str] = None,
281274
**kwargs,
282-
) -> Dict[Union[str, URIRef], Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]]:
275+
) -> Dict[int, Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]]:
283276
"""
284277
:param data_graphs: Sequence of data graphs or sources to validate independently
285278
:type data_graphs: Sequence
286279
:param multi_data_graphs_mode: Optional mode hint for compatibility with validate()
287280
:type multi_data_graphs_mode: str | None
288-
:return: dict mapping each input graph identifier to its validation results
281+
:return: dict mapping each input graph index to its validation results
289282
"""
290283

291284
if not _is_multi_data_graph_input(data_graphs):
292285
raise ReportableRuntimeError("validate_each expects a sequence of data graphs to validate.")
293286
data_graph_list = list(data_graphs)
294287
if len(data_graph_list) < 1:
295288
raise ReportableRuntimeError("No data graphs were provided for validate_each.")
296-
results: Dict[Union[str, URIRef], Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]] = {}
297-
for data_graph in data_graph_list:
289+
results: Dict[int, Tuple[bool, Union[GraphLike, bytes, ValidationFailure], str]] = {}
290+
for datagraph_i, data_graph in enumerate(data_graph_list):
298291
result = validate(
299292
data_graph,
300293
*args,
@@ -310,10 +303,9 @@ def validate_each(
310303
sparql_mode=sparql_mode,
311304
focus_nodes=focus_nodes,
312305
use_shapes=use_shapes,
313-
multi_data_graphs_mode=multi_data_graphs_mode,
314306
**kwargs,
315307
)
316-
results[_multi_data_graph_key(data_graph)] = result
308+
results[datagraph_i] = result
317309
return results
318310

319311

@@ -416,10 +408,18 @@ def shacl_rules(
416408
else:
417409
ephemeral = False
418410
use_js = kwargs.pop('js', None)
419-
# force no owl imports on data_graph
420-
loaded_dg = load_from_source(
421-
data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log
422-
)
411+
if isinstance(data_graph, DataGraph):
412+
loaded_dg = data_graph.impl
413+
dg = data_graph
414+
elif has_oxigraph and isinstance(data_graph, ox_Store):
415+
loaded_dg = data_graph
416+
dg = DataGraph.from_oxigraph_store(loaded_dg)
417+
else:
418+
# force no owl imports on data_graph
419+
loaded_dg = load_from_source(
420+
data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log
421+
)
422+
dg = DataGraph.from_rdflib(loaded_dg)
423423
ont_graph_format = kwargs.pop('ont_graph_format', None)
424424
if ont_graph is not None:
425425
loaded_og = load_from_source(
@@ -450,7 +450,7 @@ def shacl_rules(
450450
serialize_expanded_graph = kwargs.get('serialize_expanded_graph', None)
451451
try:
452452
runner = RuleExpandRunner(
453-
loaded_dg,
453+
dg,
454454
shacl_graph=loaded_sg,
455455
ont_graph=loaded_og,
456456
options=runner_options_dict,
@@ -466,7 +466,7 @@ def shacl_rules(
466466
g.add((URIRef("<urn:rdflib:pyshacl:shacl-rules-error>"), SH.message, Literal(error)))
467467
return g
468468
if serialize_expanded_graph:
469-
guess_format = "trig" if isinstance(expanded_graph, (Dataset, ConjunctiveGraph)) else "turtle"
469+
guess_format = "trig" if isinstance(expanded_graph, Dataset) else "turtle"
470470
serialize_format = kwargs.get('serialize_expanded_graph_format', guess_format)
471471
return expanded_graph.serialize(format=serialize_format)
472472
return expanded_graph

pyshacl/extras/js/function.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
#
22
#
3+
from __future__ import annotations
4+
5+
import functools
36
import typing
7+
from typing import Dict, Union
48

5-
from rdflib.plugins.sparql.operators import register_custom_function, unregister_custom_function
69
from rdflib.plugins.sparql.sparql import SPARQLError
710

8-
from pyshacl.errors import ReportableRuntimeError
9-
from pyshacl.functions.shacl_function import SHACLFunction
10-
11+
from ...errors import ReportableRuntimeError
12+
from ...functions.shacl_function import SHACLFunction
13+
from ...graph_abstraction import has_oxigraph, to_ox, to_rdf
1114
from .js_executable import JSExecutable
1215

1316
if typing.TYPE_CHECKING:
14-
from pyshacl.shapes_graph import ShapesGraph
17+
from ...graph_abstraction import DataGraph
18+
from ...shapes_graph import ShapesGraph
19+
if has_oxigraph:
20+
from pyoxigraph import BlankNode as ox_BlankNode
21+
from pyoxigraph import NamedNode as ox_NamedNode
22+
from pyoxigraph import Triple as ox_Triple
23+
else:
24+
ox_BlankNode = None
25+
ox_NamedNode = None
26+
ox_Triple = None
1527

1628

1729
class JSFunction(SHACLFunction):
@@ -56,10 +68,59 @@ def execute_from_sparql(self, e, ctx):
5668
res = results['_result']
5769
return res
5870

59-
def apply(self, g):
71+
def execute_oxigraph(
72+
self,
73+
g: 'DataGraph',
74+
args_map: Dict[str, Union[ox_NamedNode, ox_BlankNode, ox_Triple]],
75+
):
76+
"""Run the SHACL-JS function body and return a pyoxigraph term."""
77+
rdf_args_map = {k: to_rdf(v) if v is not None else None for k, v in args_map.items()}
78+
results = self.js_exe.execute(g, rdf_args_map, mode="function", return_type=self.rtype)
79+
res = results['_result']
80+
if res is None:
81+
return None
82+
return to_ox(res)
83+
84+
def execute_from_sparql_oxigraph(
85+
self,
86+
g: 'DataGraph',
87+
*args: Union[ox_NamedNode, ox_BlankNode, ox_Triple],
88+
):
89+
"""Oxigraph custom-function callback: evaluated argument terms in, one RDF term out."""
90+
if not g.is_oxigraph:
91+
raise ReportableRuntimeError("execute_from_sparql_oxigraph requires an Oxigraph-backed DataGraph.")
92+
params = self.get_params_in_order()
93+
num_params = len(params)
94+
num_args = len(args)
95+
if num_args > num_params:
96+
raise ValueError("Too many parameters passed to JSFunction {}.".format(self.node))
97+
if num_args < num_params:
98+
raise ValueError("Too few parameters passed to JSFunction {}.".format(self.node))
99+
args_map: Dict[str, Union[ox_NamedNode, ox_BlankNode, ox_Triple]] = {}
100+
for i, p in enumerate(params):
101+
ox_arg = args[i]
102+
ln = p.localname
103+
if ox_arg is None and p.optional is False:
104+
raise ReportableRuntimeError("Got NoneType for Non-optional argument {}.".format(ln))
105+
args_map[ln] = ox_arg
106+
return self.execute_oxigraph(g, args_map)
107+
108+
def apply(self, g: 'DataGraph'):
60109
super(JSFunction, self).apply(g)
61-
register_custom_function(self.node, self.execute_from_sparql, True, True)
110+
if has_oxigraph:
111+
g.register_custom_function(
112+
self.node,
113+
self.execute_from_sparql,
114+
functools.partial(self.execute_from_sparql_oxigraph, g),
115+
True,
116+
True,
117+
)
118+
else:
119+
g.register_custom_function(self.node, self.execute_from_sparql, None, True, True)
62120

63-
def unapply(self, g):
121+
def unapply(self, g: 'DataGraph'):
64122
super(JSFunction, self).unapply(g)
65-
unregister_custom_function(self.node, self.execute_from_sparql)
123+
if has_oxigraph:
124+
g.unregister_custom_function(self.node, self.execute_from_sparql, self.execute_from_sparql_oxigraph)
125+
else:
126+
g.unregister_custom_function(self.node, self.execute_from_sparql, None)

pyshacl/extras/js/rules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ def apply(
7575
if this_added:
7676
added += 1
7777
if added > 0:
78-
if isinstance(data_graph, (rdflib.Dataset, rdflib.ConjunctiveGraph)):
78+
if isinstance(data_graph, rdflib.Dataset):
7979
if target_graph_identifier is not None:
8080
target_graph = data_graph.get_context(target_graph_identifier)
8181
else:
82-
target_graph = data_graph.default_context
82+
target_graph = data_graph.default_graph
8383
else:
8484
target_graph = data_graph
8585
for s in sets_to_add:

0 commit comments

Comments
 (0)