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

1"""Explicit immutable data snapshots for repeated shape targeting.""" 

2 

3from rdflib import BNode, Literal, URIRef 

4 

5from . import _native 

6from .rdf import decode, input_graph 

7 

8 

9class TargetData: 

10 """One data snapshot, reusable across shapes and focus-node calls. 

11 

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 """ 

15 

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

22 

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

30 

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

53 

54 

55def prepare_target_data(data_graph): 

56 """Snapshot data once for repeated ``Shape.focus_nodes`` calls. 

57 

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