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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Python PEP 440 Versioning](https://www.python.org/d
- Fixes #301
- Validation report text output is now deterministic.
- Fixes #304
- `use_shapes` filtering no longer fails when omitted PropertyShapes are referenced by included shapes.
- Fixes #298

## [0.30.1] - 2025-03-15

Expand Down
24 changes: 20 additions & 4 deletions pyshacl/constraints/core/logical_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def evaluate(self, executor: SHACLExecutor, datagraph: GraphLike, focus_value_no
potentially_recursive = self.recursion_triggers(_evaluation_path)

for not_c in self.not_list:
if self.shape.sg.is_filtered_out_shape(not_c):
continue
_nc, _r = self._evaluate_not_constraint(
executor, not_c, datagraph, focus_value_nodes, potentially_recursive, _evaluation_path
)
Expand All @@ -101,19 +103,20 @@ def _evaluate_not_constraint(
"""
_reports = []
_non_conformant = False
not_shape = self.shape.get_other_shape(not_c)
if not not_shape:
found_not_shape = self.shape.get_other_shape(not_c)
if not found_not_shape:
raise ReportableRuntimeError(
"Shape pointed to by sh:not does not exist or is not a well-formed SHACL Shape."
f"Please check if the shape '{not_c}' is defined."
)
if potentially_recursive and not_shape in potentially_recursive:
if potentially_recursive and found_not_shape in potentially_recursive:
warn(ShapeRecursionWarning(_evaluation_path))
return _non_conformant, _reports
upstream_reports = []
for f, value_nodes in focus_value_nodes.items():
for v in value_nodes:
try:
_is_conform, _r = not_shape.validate(
_is_conform, _r = found_not_shape.validate(
executor, datagraph, focus=v, _evaluation_path=_evaluation_path[:]
)
except ValidationFailure as e:
Expand Down Expand Up @@ -213,12 +216,17 @@ def _evaluate_and_constraint(self, executor, and_c, target_graph, focus_value_no
raise ReportableRuntimeError("The list associated with sh:and is not a valid RDF list.")
and_shapes = set()
for a in and_list:
if self.shape.sg.is_filtered_out_shape(a):
continue
and_shape = self.shape.get_other_shape(a)
if not and_shape:
raise ReportableRuntimeError(
"Shape pointed to by sh:and does not exist or is not a well-formed SHACL Shape."
)
and_shapes.add(and_shape)
if not and_shapes:
# All filtered out, no reports to send
return _non_conformant, _reports
upstream_reports = []
for f, value_nodes in focus_value_nodes.items():
for v in value_nodes:
Expand Down Expand Up @@ -323,12 +331,16 @@ def _evaluate_or_constraint(self, executor, or_c, target_graph, focus_value_node
raise ReportableRuntimeError("The list associated with sh:or is not a valid RDF list.")
or_shapes = set()
for o in or_list:
if self.shape.sg.is_filtered_out_shape(o):
continue
or_shape = self.shape.get_other_shape(o)
if not or_shape:
raise ReportableRuntimeError(
"Shape pointed to by sh:or does not exist or is not a well-formed SHACL Shape."
)
or_shapes.add(or_shape)
if not or_shapes:
return _non_conformant, _reports
upstream_reports = []
for f, value_nodes in focus_value_nodes.items():
for v in value_nodes:
Expand Down Expand Up @@ -435,12 +447,16 @@ def _evaluate_xone_constraint(self, executor, xone_c, target_graph, focus_value_
raise ReportableRuntimeError("The list associated with sh:xone is not a valid RDF list.")
xone_shapes = list()
for x in xone_list:
if self.shape.sg.is_filtered_out_shape(x):
continue
xone_shape = self.shape.get_other_shape(x)
if not xone_shape:
raise ReportableRuntimeError(
"Shape pointed to by sh:xone does not exist or is not a well-formed SHACL Shape."
)
xone_shapes.append(xone_shape)
if not xone_shapes:
return _non_conformant, _reports
upstream_reports = []
for f, value_nodes in focus_value_nodes.items():
for v in value_nodes:
Expand Down
2 changes: 2 additions & 0 deletions pyshacl/constraints/core/other_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ def evaluate(

working_shapes = set()
for p_shape in self.property_shapes:
if self.shape.sg.is_filtered_out_shape(p_shape):
continue
property_shape = self.shape.get_other_shape(p_shape)
if not property_shape or not property_shape.is_property_shape:
raise ReportableRuntimeError(
Expand Down
30 changes: 20 additions & 10 deletions pyshacl/constraints/core/shape_based_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def evaluate(
potentially_recursive = self.recursion_triggers(_evaluation_path)

for p_shape in self.property_shapes:
if self.shape.sg.is_filtered_out_shape(p_shape):
continue
_nc, _r = self._evaluate_property_shape(
executor, p_shape, target_graph, focus_value_nodes, potentially_recursive, _evaluation_path
)
Expand All @@ -108,24 +110,24 @@ def _evaluate_property_shape(
):
_reports = []
_non_conformant = False
prop_shape = self.shape.get_other_shape(prop_shape)
if potentially_recursive and prop_shape in potentially_recursive:
found_prop_shape = self.shape.get_other_shape(prop_shape)
if potentially_recursive and found_prop_shape in potentially_recursive:
warn(ShapeRecursionWarning(_evaluation_path))
return _non_conformant, _reports
if not prop_shape:
if not found_prop_shape:
raise ReportableRuntimeError(
f"SHACL PropertyShape not found: The shape referenced by sh:property does not exist. "
f"Please check if the shape '{prop_shape}' is defined."
)
elif not prop_shape.is_property_shape:
elif not found_prop_shape.is_property_shape:
raise ReportableRuntimeError(
f"'{prop_shape}' exists but is not a well-formed SHACL PropertyShape. "
f"Ensure it has the correct type (sh:PropertyShape) and all required properties."
)

for f, value_nodes in focus_value_nodes.items():
for v in value_nodes:
_is_conform, _r = prop_shape.validate(
_is_conform, _r = found_prop_shape.validate(
executor, target_graph, focus=v, _evaluation_path=_evaluation_path[:]
)
_non_conformant = _non_conformant or (not _is_conform)
Expand Down Expand Up @@ -195,6 +197,8 @@ def evaluate(
potentially_recursive = self.recursion_triggers(_evaluation_path)

for n_shape in self.node_shapes:
if self.shape.sg.is_filtered_out_shape(n_shape):
continue
_nc, _r = self._evaluate_node_shape(
executor, n_shape, target_graph, focus_value_nodes, potentially_recursive, _evaluation_path
)
Expand All @@ -207,17 +211,20 @@ def _evaluate_node_shape(
):
_reports = []
_non_conformant = False
node_shape = self.shape.get_other_shape(node_shape)
if potentially_recursive and node_shape in potentially_recursive:
found_node_shape = self.shape.get_other_shape(node_shape)
if potentially_recursive and found_node_shape in potentially_recursive:
warn(ShapeRecursionWarning(_evaluation_path))
return _non_conformant, _reports
if not node_shape or node_shape.is_property_shape:
if not found_node_shape:
raise ReportableRuntimeError(
"Shape pointed to by sh:node does not exist or is not a well-formed SHACL NodeShape."
f"SHACL Shape not found: The shape referenced by sh:node does not exist. "
f"Please check if the shape '{node_shape}' is defined."
)
elif found_node_shape.is_property_shape:
raise ReportableRuntimeError("Shape pointed to by sh:node is not a well-formed SHACL NodeShape.")
for f, value_nodes in focus_value_nodes.items():
for v in value_nodes:
_is_conform, _r = node_shape.validate(
_is_conform, _r = found_node_shape.validate(
executor, target_graph, focus=v, _evaluation_path=_evaluation_path[:]
)
# Create a failure for this constraint component if any failures exist
Expand Down Expand Up @@ -363,6 +370,8 @@ def evaluate(
potentially_recursive = self.recursion_triggers(_evaluation_path)

for v_shape in self.value_shapes:
if self.shape.sg.is_filtered_out_shape(v_shape):
continue
_nc, _r = self._evaluate_value_shape(
executor, v_shape, target_graph, focus_value_nodes, potentially_recursive, _evaluation_path
)
Expand Down Expand Up @@ -398,6 +407,7 @@ def _evaluate_value_shape(
sibling_shapes.add(sibling)

sibling_shapes = set(self.shape.get_other_shape(s) for s in sibling_shapes)
sibling_shapes = {s for s in sibling_shapes if s is not None}
else:
sibling_shapes = set()
upstream_reports = []
Expand Down
2 changes: 2 additions & 0 deletions pyshacl/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ def set_advanced(self, val):
self._advanced = bool(val)

def get_other_shape(self, shape_node):
if self.sg.is_filtered_out_shape(shape_node):
return None
try:
return self.sg.lookup_shape_from_node(shape_node)
except (KeyError, AttributeError):
Expand Down
17 changes: 17 additions & 0 deletions pyshacl/shapes_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(self, graph, debug: Optional[Union[bool, int]] = False, logger: Opt
self._custom_constraints = None
self._shacl_functions: Dict[str, tuple] = {}
self._shacl_target_types: Dict[str, 'RDFNode'] = {}
self._filtered_out_shapes: set = set()
self._use_js = False
self._add_system_triples()

Expand Down Expand Up @@ -182,6 +183,9 @@ def shapes_from_uris(self, shapes_uris: List[rdflib.URIRef]):
self._build_node_shape_cache_from_list(shapes_uris)
return [self._node_shape_cache[s] for s in shapes_uris]

def is_filtered_out_shape(self, node) -> bool:
return isinstance(node, rdflib.URIRef) and node in self._filtered_out_shapes

def lookup_shape_from_node(self, node) -> Shape:
# This will throw a KeyError if it is not found. This is intentionally not caught here.
return self._node_shape_cache[node]
Expand All @@ -201,6 +205,7 @@ def _build_node_shape_cache(self):
:rtype: NoneType
"""
g = self.graph
self._filtered_out_shapes = set()
defined_node_shapes = set(g.subjects(RDF_type, SH_NodeShape))
if self.debug:
self.logger.debug(f"Found {len(defined_node_shapes)} SHACL Shapes defined with type sh:NodeShape.")
Expand Down Expand Up @@ -428,6 +433,18 @@ def _gather_shapes(shapes_nodes: Sequence[Union[rdflib.URIRef, rdflib.BNode]], r

_gather_shapes(shapes_list)

allowed_shapes = set(gathered_node_shapes).union(set(gathered_prop_shapes))
allowed_shape_uris = {s for s in allowed_shapes if isinstance(s, rdflib.URIRef)}
referenced_shape_uris = set()
for _p in (SH_property, SH_node, SH_not, SH_qualifiedValueShape):
referenced_shape_uris.update(o for o in g.objects(None, _p) if isinstance(o, rdflib.URIRef))
for _p in (SH_and, SH_or, SH_xone):
for list_node in g.objects(None, _p):
for item in g.items(list_node):
if isinstance(item, rdflib.URIRef):
referenced_shape_uris.add(item)
self._filtered_out_shapes = referenced_shape_uris.difference(allowed_shape_uris)

for s in gathered_node_shapes:
path_vals = list(g.objects(s, SH_path))
if len(path_vals) > 0:
Expand Down
70 changes: 70 additions & 0 deletions test/issues/test_298.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
"""
https://github.com/RDFLib/pySHACL/issues/298
"""
import sys

from rdflib import Graph
from rdflib.plugins.parsers.jsonld import to_rdf

import pyshacl


def test_298():
shapes_graph = to_rdf(
{
"@context": {
"ex": "http://example.org/",
"sh": "http://www.w3.org/ns/shacl#",
},
"@graph": [
{
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": {"@id": "ex:Person"},
"sh:property": [
{
"@id": "ex:NameProperty",
"sh:path": {"@id": "ex:name"},
"sh:minCount": 1,
},
{
"@id": "ex:AgeProperty",
"sh:path": {"@id": "ex:age"},
"sh:minInclusive": 18,
},
],
}
],
},
Graph(),
)

data_graph = to_rdf(
{
"@context": {
"ex": "http://example.org/",
},
"@id": "ex:person1",
"@type": "ex:Person",
"ex:name": "John Doe",
"ex:age": 25,
},
Graph(),
)

conforms, report_graph, results_text = pyshacl.validate(
data_graph,
shacl_graph=shapes_graph,
use_shapes=[
"http://example.org/PersonShape",
"http://example.org/NameProperty",
],
)
assert conforms
assert "Validation Report" in results_text


if __name__ == "__main__":
sys.exit(test_298())
1 change: 0 additions & 1 deletion test/issues/test_301.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,4 @@ def test_301():


if __name__ == "__main__":
test_301()
sys.exit(test_301())
3 changes: 3 additions & 0 deletions test/issues/test_304.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,6 @@ def test_304():
assert "Results (" in out_a
assert out_a == out_b
assert out_a == out_c

if __name__ == "__main__":
sys.exit(test_304())
Loading