Coverage for python/pyshacl_turbo/target_data.py: 100.0%
36 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 12:25 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 12:25 +0000
1"""Explicit immutable data snapshots for repeated shape targeting."""
3from rdflib import BNode, Literal, URIRef
5from . import _native
6from .rdf import decode, input_graph
9class TargetData:
10 """One data snapshot, reusable across shapes and focus-node calls.
12 Changes to the original graph are visible only after preparing a new snapshot.
13 Cached target sets belong to this snapshot and the immutable compiled shapes.
14 """
16 def __init__(self, data_graph):
17 data = input_graph(data_graph) 1jklmnopqrabcdefgstvhi
18 self._native = _native.TargetData(data.source, data.format, data.base) 1jklmnopqrabcdefgstvhi
19 self._blank_nodes = data.blank_nodes 1jklmnopqrabcdefgstvhi
20 self._results = {} 1jklmnopqrabcdefgstvhi
21 self._targeting_indexes = {} 1jklmnopqrabcdefgstvhi
23 def _focus_nodes(self, compiled, node, advanced):
24 key = (compiled, node, advanced) 1jklmnopqr
25 if key not in self._results: 1jklmnopqr
26 output = compiled._native.focus_nodes(node.n3(), self._native, advanced) 1jklmnopqr
27 result = decode(output, compiled._blank_nodes | self._blank_nodes, preserve_ids=True) 1jklmnopqr
28 self._results[key] = frozenset(result.objects()) 1jklmnopqr
29 return set(self._results[key]) 1jklmnopqr
31 def _targeting(self, compiled, node, skip):
32 if not isinstance(node, (URIRef, BNode, Literal)): 1abcdefgstvhi
33 raise TypeError("node must be an RDFLib IRI, blank node, or literal") 1v
34 if isinstance(skip, (str, BNode, Literal)): 1abcdefgsthi
35 raise TypeError("skip must be an iterable of shape IRIs or blank nodes") 1t
36 skip = frozenset(skip) 1abcdefgshi
37 if any(not isinstance(shape, (URIRef, BNode)) for shape in skip): 1abcdefgshi
38 raise TypeError("skip must contain only shape IRIs or blank nodes") 1s
39 key = (compiled, skip) 1abcdefghi
40 if key not in self._targeting_indexes: 1abcdefghi
41 output = compiled._native.targeting_index( 1abcdefghi
42 self._native, sorted(shape.n3() for shape in skip)
43 )
44 result = decode(output, compiled._blank_nodes | self._blank_nodes, preserve_ids=True) 1abcdefghi
45 index = {} 1abcdefghi
46 for shape, _, focus in result: 1abcdefghi
47 index.setdefault(focus, set()).add(shape) 1abcdefghi
48 self._targeting_indexes[key] = { 1abcdefghi
49 focus: tuple(sorted(shapes, key=lambda shape: shape.n3()))
50 for focus, shapes in index.items()
51 }
52 return list(self._targeting_indexes[key].get(node, ())) 1abcdefghi
55def prepare_target_data(data_graph):
56 """Snapshot data once for repeated ``Shape.focus_nodes`` calls.
58 Preparing an existing TargetData returns it unchanged. Prepare the original
59 graph again when its mutations should become visible.
60 """
61 return data_graph if isinstance(data_graph, TargetData) else TargetData(data_graph) 1jklmnopqrabcdefgstvhi