Coverage for lode / reader / logic / rdf_logic.py: 12%
142 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-10-05 16:02 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-10-05 16:02 +0000
1# rdf_logic.py
3from rdflib import URIRef, Node, Literal as RDFlibLiteral, BNode
4from rdflib.namespace import RDF, RDFS, OWL, SKOS, XSD
5from rdflib.collection import Collection as RDFLibCollection
7from lode.models import *
8from lode.reader.logic.base_logic import BaseLogic
11class RdfLogic(BaseLogic):
12 """
13 RDF/RDFS parsing logic.
15 Behavior:
16 - Phase1: classify URIRef subjects of mapped predicates whose config
17 declares inferred_class or a single target_classes (mirrors OwlLogic).
18 BNodes with inferred_class are classified too (e.g. reified Statements
19 via rdf:subject/predicate/object).
20 - Phase2: create instances from rdf:type using type_mapping; static
21 setters declared in 'is: class' entries are applied immediately.
22 - Phase3: populate via setters/handlers from the property mapping.
23 - Phase4: no group axioms in pure RDF/RDFS.
24 - Phase5: fallback - all unmapped predicates -> Property, all unmapped
25 URI subjects/objects -> Resource, then RDFS defaults applied to every
26 Property (rdfs:Resource for domain, rdfs:Class for range) climbing the
27 rdfs:subPropertyOf hierarchy first.
28 - Phase6: inherited from BaseLogic, reifies unmapped triples as Statements.
29 """
31 # ========== READER PHASES ==========
33 def phase1_classify_from_predicates(self):
34 """Classifies subjects of mapped predicates.
35 - BNodes with inferred_class predicates -> classified
36 - URIRefs with inferred_class or unambiguous target_classes -> classified
37 """
38 classified = {}
40 for pred in self._property_mapping:
41 for uri in self.graph.subjects(pred, None):
42 if uri in self._instance_cache:
43 continue
44 python_class = self._strategy.classify_by_predicate(uri, self.graph)
45 if not python_class:
46 continue
47 if isinstance(uri, BNode):
48 if uri not in classified:
49 classified[uri] = python_class
50 elif isinstance(uri, URIRef):
51 self.get_or_create(uri, python_class, populate=False)
53 for uri, py_class in classified.items():
54 self.get_or_create(uri, py_class, populate=False)
56 def phase2_create_from_types(self):
57 type_mapping = self._strategy.get_type_mapping()
59 for rdf_type, config in type_mapping.items():
60 py_class = config.get('target_class')
61 if not py_class:
62 continue
63 for uri in self.graph.subjects(RDF.type, rdf_type):
64 self.get_or_create(uri, py_class, populate=False)
65 for instance in self._instance_cache.get(uri, set()):
66 if 'setters' in config:
67 self._apply_setters_immediate(instance, config['setters'])
68 if 'handler' in config:
69 getattr(self, config['handler'])(instance, uri)
71 def phase3_populate_properties(self):
72 """Populates each cached instance via configured setters and handlers."""
73 for uri in list(self._instance_cache.keys()):
74 for instance in list(self._instance_cache[uri]):
75 self.populate_instance(instance, uri)
77 def phase4_process_group_axioms(self):
78 """No group axioms in pure RDF/RDFS."""
79 pass
81 def phase5_fallback(self):
82 """
83 Generic fallback for RDF/RDFS:
84 1. Every predicate not yet cached and outside structural namespaces
85 is created as Property.
86 2. Every URI subject/object not yet cached is created as Resource.
87 3. RDFS defaults (rdfs:Resource for domain, rdfs:Class for range)
88 are applied to every Property after climbing the rdfs:subPropertyOf
89 chain to inherit declared values when present.
90 """
91 exclude_predicates = {RDF.first, RDF.rest, RDF.nil}
93 # 1. predicati -> Property
94 for pred in set(self.graph.predicates()):
95 if pred in self._instance_cache or pred in exclude_predicates:
96 continue
97 if self._is_structural(pred):
98 continue
99 self.get_or_create(pred, Property, populate=False)
101 # 2. soggetti/oggetti URI -> Resource
102 for subj in set(self.graph.subjects()):
103 if isinstance(subj, URIRef) and subj not in self._instance_cache:
104 if self._is_structural(subj) or self._is_rdf_collection(subj):
105 continue
106 self.get_or_create(subj, Resource, populate=False)
108 for obj in set(self.graph.objects()):
109 if isinstance(obj, URIRef) and obj not in self._instance_cache:
110 if self._is_structural(obj) or self._is_rdf_collection(obj):
111 continue
112 self.get_or_create(obj, Resource, populate=False)
114 # 3. defaults RDFS su Property
115 for uri, instances in list(self._instance_cache.items()):
116 for instance in list(instances):
117 if isinstance(instance, Property):
118 self._apply_rdfs_defaults(instance)
120 # ========== HELPERS ==========
122 def _is_structural(self, uri: Node) -> bool:
123 """True if URI belongs to RDF/RDFS/OWL/SKOS/XSD vocabulary namespaces."""
124 if not isinstance(uri, URIRef):
125 return False
126 s = str(uri)
127 return any(s.startswith(ns) for ns in (str(RDF), str(RDFS), str(OWL), str(SKOS), str(XSD)))
129 def _apply_setters_immediate(self, instance, setters_config):
130 """Applies static setter values from config directly (mirror of
131 OwlLogic._apply_setters_immediate). Used in phase2 for 'is: class'
132 entries that declare setters with constant values.
133 """
134 for setter_item in setters_config:
135 if isinstance(setter_item, dict):
136 for setter_name, value in setter_item.items():
137 if hasattr(instance, setter_name):
138 getattr(instance, setter_name)(value)
139 else:
140 if hasattr(instance, setter_item):
141 getattr(instance, setter_item)()
143 def _apply_rdfs_defaults(self, instance):
144 """Applies rdfs:Resource (domain) and rdfs:Class (range) defaults
145 when the property does not declare them, after walking up
146 rdfs:subPropertyOf to inherit values from ancestors.
147 """
148 if not instance.get_has_domain():
149 inherited = self._get_inherited_property_values(instance, "get_has_domain")
150 if inherited:
151 for d in inherited:
152 instance.set_has_domain(d)
153 else:
154 instance.set_has_domain(self.get_or_create(RDFS.Resource, Concept))
156 if not instance.get_has_range():
157 inherited = self._get_inherited_property_values(instance, "get_has_range")
158 if inherited:
159 for r in inherited:
160 instance.set_has_range(r)
161 else:
162 instance.set_has_range(self.get_or_create(RDFS.Class, Concept))
164 def _get_inherited_property_values(self, property_instance, getter_name: str) -> list:
165 """Walks up rdfs:subPropertyOf collecting the first non-empty value
166 of getter_name found on an ancestor.
167 """
168 def collect(node):
169 if node is property_instance:
170 return None
171 getter = getattr(node, getter_name, None)
172 if getter:
173 values = getter()
174 if values:
175 return values if isinstance(values, list) else [values]
176 return None
178 result = self._traverse_hierarchy(
179 property_instance,
180 next_getter="get_is_sub_property_of",
181 direction="up",
182 collect=collect,
183 )
184 return result if result is not None else []
187 def create_python_container(self, instance, uri):
188 """Populates a Container instance from RDF container/list syntax.
190 Handles all four RDF container types:
191 - rdf:Bag, rdf:Alt, rdf:Seq -> rdf:_N indexed members
192 - rdf:List -> rdf:first/rdf:rest linked list
194 URI members are resolved as Resource, RDFlibLiterals as Literal.
195 All consumed triples are registered in _triples_map so phase6 does not
196 reify them as Statements.
197 """
198 if instance not in self._triples_map:
199 self._triples_map[instance] = set()
201 # rdf:List: rdf:first/rdf:rest chain
202 if (uri, RDF.first, None) in self.graph:
203 try:
204 for item in RDFLibCollection(self.graph, uri):
205 member = self._resolve_container_member(item)
206 if member:
207 instance.set_has_member(member)
208 # mark structural triples as consumed
209 node = uri
210 while node and node != RDF.nil:
211 first = self.graph.value(node, RDF.first)
212 rest = self.graph.value(node, RDF.rest)
213 if first is not None:
214 self._triples_map[instance].add((node, RDF.first, first))
215 if rest is not None:
216 self._triples_map[instance].add((node, RDF.rest, rest))
217 node = rest
218 except Exception as e:
219 print(f"Errore create_python_container (List): {e}")
220 return
222 # rdf:Bag/Alt/Seq: rdf:_N indexed members
223 indexed = []
224 for pred, obj in self.graph.predicate_objects(uri):
225 pred_str = str(pred)
226 prefix = str(RDF) + "_"
227 if pred_str.startswith(prefix):
228 try:
229 idx = int(pred_str[len(prefix):])
230 indexed.append((idx, pred, obj))
231 except ValueError:
232 continue
234 indexed.sort(key=lambda t: t[0])
235 for _, pred, obj in indexed:
236 member = self._resolve_container_member(obj)
237 if member:
238 instance.set_has_member(member)
239 self._triples_map[instance].add((uri, pred, obj))
241 def _resolve_container_member(self, node):
242 if isinstance(node, RDFlibLiteral):
243 return self._create_literal(node)
244 return self.get_or_create(node, Resource)