Coverage for lode / reader / logic / owl_logic.py: 73%

658 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-10-05 16:02 +0000

1# owl_logic.py 

2 

3from rdflib import Graph, URIRef, Node, Literal as RDFlibLiteral, BNode 

4from rdflib.namespace import RDF, RDFS, OWL, SKOS, XSD 

5from rdflib.collection import Collection as RDFLibCollection 

6 

7from lode.models import * 

8from lode.reader.logic.base_logic import BaseLogic 

9from lode.reader.warnings import owl_warnings 

10 

11 

12class OwlLogic(BaseLogic): 

13 """ 

14 OWL-specific parsing logic. 

15 

16 Extends BaseLogic with: 

17 - Phase 1 BNode classification for OWL restrictions and truth functions 

18 - OWL defaults for domain, range (owl:Thing) and Individual type 

19 - Handlers for property characteristics, cardinality, truth functions, 

20 quantifiers, and group axioms (AllDisjointClasses, AllDifferent, etc.) 

21 - Property type inference for properties not explicitly typed in the artefact 

22 """ 

23 

24 # _get_allowed_namespaces: ereditato da BaseLogic, legge config YAML 

25 

26 # ========== HOOK per resolve custom OWL ========== 

27 

28 def _pre_resolve_hook(self, python_class: type, id: Node) -> type | None: 

29 """ 

30 OWL override: if the URI is already cached with a non-Individual type, 

31 return that type to prevent silent downcasting during MRO resolution. 

32 Returns None to fall through to the default MRO walk. 

33 """ 

34 if id and id in self._instance_cache: 

35 for existing in self._instance_cache[id]: 

36 if type(existing) is not Individual: 

37 return type(existing) 

38 return None 

39 

40 # ========== READER PHASES ========== 

41 

42 def phase1_classify_from_predicates(self): 

43 """Scans the RDF graph for subjects of mapped predicates. 

44 - BNodes with inferred_class predicates -> classified and registered (mainly Restrictions) 

45 - URIRefs with any mapped predicate but no rdf:type -> registered with 

46 the inferred type from classify_by_predicate (only if unambiguous) 

47 """ 

48 

49 classified = {} 

50 

51 for pred in self._property_mapping: 

52 for uri in self.graph.subjects(pred, None): 

53 if uri in self._instance_cache: 

54 continue 

55 python_class = self._strategy.classify_by_predicate(uri, self.graph) 

56 if not python_class: 

57 continue 

58 if isinstance(uri, BNode): 

59 # [ owl:inverseOf <prop> ] used as object of owl:onProperty is an 

60 # anonymous inline property expression inside a Restriction, not a 

61 # standalone entity — skipped it to avoid spurious cache entries 

62 if (None, OWL.onProperty, uri) in self.graph: 

63 continue 

64 if uri not in classified: 

65 classified[uri] = python_class 

66 elif isinstance(uri, URIRef): 

67 if not issubclass(python_class, Restriction): 

68 self.get_or_create(uri, python_class, populate=False) 

69 

70 owl_warnings.flag_malformed_restrictions(self, classified) 

71 

72 self._classify_nested(classified, self._strategy.get_classifier_predicates()) 

73 

74 for uri, py_class in classified.items(): 

75 self.get_or_create(uri, py_class, populate=False) 

76 

77 def phase2_create_from_types(self): 

78 type_mapping = self._strategy.get_type_mapping() 

79 

80 for rdf_type, config in type_mapping.items(): 

81 py_class = config.get('target_class') 

82 if not py_class: 

83 continue 

84 for uri in self.graph.subjects(RDF.type, rdf_type): 

85 inst = self.get_or_create(uri, py_class, populate=False) 

86 if inst is not None: 

87 self._track(inst, (uri, RDF.type, rdf_type)) 

88 # Apply static setters (e.g. set_is_transitive: True) always, 

89 # even if instance already existed in cache from phase1. 

90 if 'setters' in config: 

91 for instance in self._instance_cache.get(uri, set()): 

92 self._apply_setters_immediate(instance, config['setters']) 

93 

94 # Subjects with rdf:type not covered by config -> Individual 

95 for s, o in self.graph.subject_objects(RDF.type): 

96 if isinstance(s, BNode) or s in self._instance_cache: 

97 continue 

98 if o not in type_mapping: 

99 if isinstance(o, URIRef): 

100 o_str = str(o) 

101 if any(o_str.startswith(ns) for ns in self._allowed_namespaces) and o not in (OWL.Thing, OWL.Nothing, RDFS.Literal): 

102 continue 

103 self.get_or_create(s, Individual, populate=True) 

104 

105 # Subjects with an rdf:type not mapped in config -> Individual 

106 for s, o in self.graph.subject_objects(RDF.type): 

107 if o not in type_mapping and not isinstance(s, BNode) and s not in self._instance_cache: 

108 self.get_or_create(s, Individual, populate=True) 

109 

110 def phase3_populate_properties(self): 

111 """Iterates over all cached instances and populates their properties 

112 by dispatching each predicate-object pair through the configured 

113 setters and handlers defined in the property mapping. 

114 """ 

115 

116 for uri in list(self._instance_cache.keys()): 

117 for instance in list(self._instance_cache[uri]): 

118 self.populate_instance(instance, uri) 

119 

120 def phase4_process_group_axioms(self): 

121 """Processes group axioms defined in the enricher section of the config. 

122 For each axiom type, retrieves all matching subjects from the graph 

123 and dispatches them to the handler specified in the config entry. 

124 """ 

125 

126 axioms = self._strategy.get_group_axioms() 

127 for axiom_type, handler_name in axioms.items(): 

128 for uri in self.graph.subjects(RDF.type, axiom_type): 

129 # handler existence guaranteed by _validate_handlers (defined in base_logic) 

130 getattr(self, handler_name)(uri) 

131 

132 def phase5_fallback(self): 

133 """ 

134 Classifies or reclassifies any entity whose concrete type could not be 

135 determined in earlier phases, in particular generic Property instances 

136 whose subtype (Relation, Attribute, Annotation) is not explicitly 

137 asserted in the semantic artefact via rdf:type (handled by phase 2). It uses _infer_property_type to resolve the concrete subtype. 

138 

139 After reclassification, applies OWL defaults (e.g., domain, range, thing) to all instances via _enrich_or_apply_owl_defaults. 

140 """ 

141 

142 for uri, instances in list(self._instance_cache.items()): 

143 for instance in list(instances): 

144 

145 if type(instance) is Property: 

146 # Check if a more specific Property subclass already exists for this URI 

147 

148 has_concrete = any( 

149 type(i) in (Relation, Attribute, Annotation) 

150 for i in instances if i is not instance 

151 ) 

152 

153 if has_concrete: 

154 # Remove generic Property, keep the concrete one 

155 self._instance_cache[uri].discard(instance) 

156 else: 

157 inferred = self._infer_property_type(instance) 

158 old_dict = instance.__dict__.copy() 

159 instance.__class__ = inferred 

160 inferred.__init__(instance) 

161 instance.__dict__.update(old_dict) 

162 self.populate_instance(instance, uri) 

163 

164 self._enrich_or_apply_owl_defaults(instance, uri) 

165 

166 def handle_domain(self, instance, uri, predicate, obj, setter=None): 

167 concept = self.get_or_create(obj, Concept) 

168 if concept is None: 

169 concept = self._force_as_concept(obj) 

170 instance.set_has_domain(concept) 

171 concept.set_is_in_domain_of(instance) 

172 

173 

174 def handle_range(self, instance, uri, predicate, obj, setter=None): 

175 if isinstance(instance, Attribute): 

176 resource = self.get_or_create(obj, Datatype) 

177 else: 

178 resource = self.get_or_create(obj, Concept) 

179 if resource is None: 

180 resource = self._force_as_concept(obj) 

181 instance.set_has_range(resource) 

182 resource.set_is_in_range_of(instance) 

183 

184 def handle_equivalent_class(self, instance, uri, predicate, obj, setter=None): 

185 """§9.1.2 EquivalentClasses — simmetria garantita.""" 

186 

187 # add WARNING 

188 if not isinstance(instance, Concept): 

189 return 

190 

191 other = self.get_or_create(obj, Concept) 

192 # add WARNING  

193 if not other or not isinstance(other, Concept): 

194 return 

195 

196 if other not in instance.get_is_equivalent_to(): 

197 instance.set_is_equivalent_to(other) 

198 if instance not in other.get_is_equivalent_to(): 

199 other.set_is_equivalent_to(instance) 

200 

201 def handle_disjoint_with(self, instance, uri, predicate, obj, setter=None): 

202 """§9.1.3 DisjointClasses — simmetria garantita.""" 

203 other = self.get_or_create(obj, Concept) 

204 if other: 

205 if other not in instance.get_is_disjoint_with(): 

206 instance.set_is_disjoint_with(other) 

207 if instance not in other.get_is_disjoint_with(): 

208 other.set_is_disjoint_with(instance) 

209 

210 def handle_equivalent_property(self, instance, uri, predicate, obj, setter=None): 

211 if obj in self._instance_cache: 

212 other = self.get_or_create(obj, Property) 

213 else: 

214 other = self.get_or_create(obj, type(instance)) 

215 if other: 

216 if other not in instance.get_is_equivalent_to(): 

217 instance.set_is_equivalent_to(other) 

218 if instance not in other.get_is_equivalent_to(): 

219 other.set_is_equivalent_to(instance) 

220 

221 def handle_property_disjoint_with(self, instance, uri, predicate, obj, setter=None): 

222 """§9.2.3 DisjointObjectProperties — simmetria garantita.""" 

223 other = self.get_or_create(obj, Property) 

224 if other: 

225 if other not in instance.get_is_disjoint_with(): 

226 instance.set_is_disjoint_with(other) 

227 if instance not in other.get_is_disjoint_with(): 

228 other.set_is_disjoint_with(instance) 

229 

230 def _ensure_individual(self, node): 

231 """Get or create an Individual for node, preserving existing types (punning).""" 

232 if isinstance(node, RDFlibLiteral): 

233 return None 

234 cached = self._instance_cache.get(node, set()) 

235 ind = next((i for i in cached if isinstance(i, Individual)), None) 

236 if ind: 

237 return ind 

238 ind = Individual() 

239 ind.set_has_identifier(str(node)) 

240 self._instance_cache.setdefault(node, set()).add(ind) 

241 return ind 

242 

243 def handle_same_as(self, instance, uri, predicate, obj, setter=None): 

244 """§9.6.1 SameIndividual — simmetria garantita.""" 

245 subj_ind = self._ensure_individual(uri) 

246 other = self._ensure_individual(obj) 

247 if subj_ind and other: 

248 if other not in subj_ind.get_is_same_as(): 

249 subj_ind.set_is_same_as(other) 

250 if subj_ind not in other.get_is_same_as(): 

251 other.set_is_same_as(subj_ind) 

252 

253 def handle_different_from(self, instance, uri, predicate, obj, setter=None): 

254 """§9.6.2 DifferentIndividuals — simmetria garantita.""" 

255 subj_ind = self._ensure_individual(uri) 

256 other = self._ensure_individual(obj) 

257 if subj_ind and other: 

258 if other not in subj_ind.get_is_different_from(): 

259 subj_ind.set_is_different_from(other) 

260 if subj_ind not in other.get_is_different_from(): 

261 other.set_is_different_from(subj_ind) 

262 

263 def handle_inverse_of(self, instance, uri, predicate, obj, setter=None): 

264 if isinstance(instance, Restriction) and not isinstance(instance, Relation): 

265 prop = self.get_or_create(obj, Property) 

266 if prop: 

267 instance.set_applies_on_property(prop) 

268 return 

269 other = self.get_or_create(obj, Relation) 

270 if other: 

271 if instance.get_is_inverse_of() is not other: 

272 instance.set_is_inverse_of(other) 

273 if other.get_is_inverse_of() is not instance: 

274 other.set_is_inverse_of(instance) 

275 

276 def handle_on_property(self, instance, uri, predicate, obj, setter=None): 

277 """If obj is a BNode with owl:inverseOf, resolve the real property and mark as inverse. 

278 Undeclared properties default to Relation (ObjectProperty) per OWL DL. 

279 """ 

280 if isinstance(obj, BNode) and (obj, OWL.inverseOf, None) in self.graph: 

281 inverse_of = self.graph.value(obj, OWL.inverseOf) 

282 if inverse_of: 

283 prop = self.get_or_create(inverse_of, Property) 

284 if prop and type(prop) is Property: 

285 prop = self.get_or_create(inverse_of, Relation) 

286 if prop and isinstance(instance, PropertyConceptRestriction): 

287 instance.set_applies_on_property(prop) 

288 instance.set_is_inverse(True) 

289 return 

290 prop = self.get_or_create(obj, Property) 

291 if prop and type(prop) is Property: 

292 prop = self.get_or_create(obj, Relation) 

293 if prop: 

294 instance.set_applies_on_property(prop) 

295 

296 def _force_as_concept(self, uri): 

297 """Aggiunge Concept in cache per URI già classificato come Individual/Resource (punning OWL).""" 

298 cached = list(self._instance_cache.get(uri, set())) 

299 concept = Concept() 

300 if cached: 

301 concept.__dict__.update(cached[0].__dict__) 

302 else: 

303 concept.set_has_identifier(str(uri)) 

304 self._instance_cache.setdefault(uri, set()).add(concept) 

305 return concept 

306 

307 def _enrich_or_apply_owl_defaults(self, instance, uri): 

308 """ 

309 Applies OWL-mandated defaults to instances lacking explicit declarations. 

310 [...docstring invariata...] 

311 """ 

312 

313 if uri not in self._instance_cache: 

314 return 

315 

316 owl_thing = self.get_or_create(OWL.Thing, Concept) 

317 rdfs_string = self.get_or_create(RDFS.Literal, Datatype) 

318 

319 def _safe_inherited(instance, getter_name): 

320 values = self._get_inherited_property_values(instance, getter_name) 

321 if isinstance(instance, Attribute): 

322 if getter_name == "get_has_range": 

323 return [v for v in values if isinstance(v, (Datatype, DatatypeRestriction))] 

324 if isinstance(instance, Relation): 

325 if getter_name == "get_has_range": 

326 return [v for v in values if isinstance(v, Concept)] 

327 return values 

328 

329 def _is_only_default(values, default_id): 

330 return values and all(d.get_has_identifier() == default_id for d in values) 

331 

332 # Type-specific defaults: always apply, even for punning subordinates 

333 # (these are constitutive of the class, not generic Resource enrichments) 

334 

335 if isinstance(instance, Relation): 

336 current_domains = instance.get_has_domain() 

337 if not current_domains or _is_only_default(current_domains, owl_thing): 

338 inherited_domain = _safe_inherited(instance, "get_has_domain") 

339 if inherited_domain: 

340 instance.has_domain.clear() 

341 for domain in inherited_domain: 

342 instance.set_has_domain(domain) 

343 elif not current_domains: 

344 instance.set_has_domain(owl_thing) 

345 

346 current_ranges = instance.get_has_range() 

347 if not current_ranges or _is_only_default(current_ranges, owl_thing): 

348 inherited_range = _safe_inherited(instance, "get_has_range") 

349 if inherited_range: 

350 instance.has_range.clear() 

351 for r in inherited_range: 

352 instance.set_has_range(r) 

353 elif not current_ranges: 

354 instance.set_has_range(owl_thing) 

355 

356 if isinstance(instance, Attribute): 

357 current_domains = instance.get_has_domain() 

358 if not current_domains or _is_only_default(current_domains, owl_thing): 

359 inherited_domain = _safe_inherited(instance, "get_has_domain") 

360 if inherited_domain: 

361 instance.has_domain.clear() 

362 for domain in inherited_domain: 

363 instance.set_has_domain(domain) 

364 elif not current_domains: 

365 instance.set_has_domain(owl_thing) 

366 

367 current_ranges = instance.get_has_range() 

368 if not current_ranges or _is_only_default(current_ranges, rdfs_string): 

369 inherited_range = _safe_inherited(instance, "get_has_range") 

370 if inherited_range: 

371 instance.has_range.clear() 

372 for r in inherited_range: 

373 instance.set_has_range(r) 

374 elif not current_ranges: 

375 instance.set_has_range(rdfs_string) 

376 

377 if isinstance(instance, Individual): 

378 if not instance.get_has_type(): 

379 instance.set_has_type(owl_thing) 

380 

381 # Generic Resource-level enrichments: skip for punning subordinates 

382 if self._is_punning_subordinate(instance, uri): 

383 return 

384 

385 if isinstance(instance, Resource): 

386 if len(self._instance_cache[uri]) > 1: 

387 existing = instance.get_also_defined_as() or [] 

388 for other in self._instance_cache[uri]: 

389 if other is instance: 

390 continue 

391 if other not in existing: 

392 instance.set_also_defined_as(other) 

393 if instance not in (other.get_also_defined_as() or []): 

394 other.set_also_defined_as(instance) 

395 

396 

397 def _get_inherited_property_values(self, property_instance, getter_name: str) -> list: 

398 def collect(node): 

399 if node is property_instance: 

400 return None 

401 getter = getattr(node, getter_name, None) 

402 if getter: 

403 values = getter() 

404 if values: 

405 return values if isinstance(values, list) else [values] 

406 if isinstance(node, Relation): 

407 swapped_getter = {"get_has_domain": "get_has_range", "get_has_range": "get_has_domain"}.get(getter_name) 

408 if swapped_getter: 

409 for inverse in (node.get_is_inverse_of() or []): 

410 swapped = getattr(inverse, swapped_getter, lambda: None)() 

411 if swapped: 

412 return swapped if isinstance(swapped, list) else [swapped] 

413 inverse_uri = URIRef(inverse.get_has_identifier()) 

414 graph_pred = RDFS.range if swapped_getter == "get_has_range" else RDFS.domain 

415 for obj in self.graph.objects(inverse_uri, graph_pred): 

416 if isinstance(obj, BNode): 

417 continue 

418 inst = self.get_or_create(obj, Concept) 

419 if inst: 

420 return [inst] 

421 return None 

422 

423 if isinstance(property_instance, Relation): 

424 swapped_getter = {"get_has_domain": "get_has_range", "get_has_range": "get_has_domain"}.get(getter_name) 

425 if swapped_getter: 

426 owl_thing_id = str(OWL.Thing) 

427 for inverse in (property_instance.get_is_inverse_of() or []): 

428 swapped = getattr(inverse, swapped_getter, lambda: None)() 

429 if swapped: 

430 filtered = [v for v in (swapped if isinstance(swapped, list) else [swapped]) 

431 if v.get_has_identifier() != owl_thing_id] 

432 if filtered: 

433 return filtered 

434 inverse_uri = URIRef(inverse.get_has_identifier()) 

435 graph_pred = RDFS.range if swapped_getter == "get_has_range" else RDFS.domain 

436 for obj in self.graph.objects(inverse_uri, graph_pred): 

437 if isinstance(obj, BNode): 

438 continue 

439 inst = self.get_or_create(obj, Concept) 

440 if inst: 

441 return [inst] 

442 

443 result = self._traverse_hierarchy( 

444 property_instance, 

445 next_getter="get_is_sub_property_of", 

446 direction="up", 

447 collect=collect, 

448 ) 

449 return result if result is not None else [] 

450 

451 

452 def _infer_property_type(self, instance) -> type: 

453 """ 

454 Infers the concrete subtype (Relation, Attribute, Annotation) for a 

455 generic Property instance. 

456 

457 Strategy: 

458 1. owl:inverseOf in graph — if present, this is a Relation. 

459 2. Traverse UP subPropertyOf — inherit type from ancestor. 

460 3. Traverse DOWN subPropertyOf — inherit type from descendant. 

461 4. Declared rdfs:range — XSD/Literal range -> Attribute, URIRef range -> Relation. 

462 5. Graph usage — all literal objects -> Attribute, any URIRef object -> Relation. 

463 6. Fallback to Annotation. 

464 """ 

465 uri_ref = URIRef(instance.get_has_identifier()) if instance.get_has_identifier() else None 

466 

467 # (1) inverseOf in graph (instance not yet populated as Property) 

468 if uri_ref: 

469 if any(self.graph.objects(uri_ref, OWL.inverseOf)) or \ 

470 any(self.graph.subjects(OWL.inverseOf, uri_ref)): 

471 return Relation 

472 

473 CONCRETE = (Relation, Attribute, Annotation) 

474 

475 # (2) UP THE HIERARCHY 

476 def collect_type(node): 

477 if type(node) in CONCRETE: 

478 return type(node) 

479 return None 

480 

481 result = self._traverse_hierarchy( 

482 instance, 

483 next_getter="get_is_sub_property_of", 

484 direction="up", 

485 collect=collect_type, 

486 ) 

487 if result: 

488 return result 

489 

490 # (3) DOWN THE HIERARCHY 

491 result = self._traverse_hierarchy( 

492 instance, 

493 next_getter="get_is_sub_property_of", 

494 direction="down", 

495 collect=collect_type, 

496 ) 

497 if result: 

498 return result 

499 

500 # (4) Declared rdfs:range 

501 if uri_ref: 

502 for range_obj in self.graph.objects(uri_ref, RDFS.range): 

503 range_str = str(range_obj) 

504 if range_str.startswith(str(XSD)) or range_obj in (RDFS.Literal,): 

505 return Attribute 

506 if isinstance(range_obj, URIRef): 

507 return Relation 

508 

509 # (5) Graph usage: how the property is used as predicate 

510 if uri_ref: 

511 has_literal = False 

512 has_uriref = False 

513 for s, p, o in self.graph.triples((None, uri_ref, None)): 

514 if isinstance(o, RDFlibLiteral): 

515 has_literal = True 

516 elif isinstance(o, URIRef): 

517 has_uriref = True 

518 if has_uriref: 

519 return Relation 

520 if has_literal: 

521 return Attribute 

522 

523 # (6) fallback 

524 return Annotation 

525 

526 # ========== HELPERS & RELATED FUNCTIONS PHASE 1 ========== 

527 

528 def _classify_nested(self, classified, predicates): 

529 """ 

530 Recursively classifies BNodes found inside OWL list collections 

531 (owl:intersectionOf, owl:unionOf, owl:oneOf) hanging off already-classified 

532 BNodes. Extends the classified dict in place with any newly discovered 

533 BNode-to-class mappings. 

534 """ 

535 

536 list_preds = [OWL.intersectionOf, OWL.unionOf, OWL.oneOf, OWL.onDataRange] 

537 

538 for bnode in list(classified.keys()): 

539 for pred, obj in self.graph.predicate_objects(bnode): 

540 if pred in list_preds: 

541 try: 

542 collection = RDFLibCollection(self.graph, obj) 

543 for item in collection: 

544 if isinstance(item, BNode) and item not in classified: 

545 py_class = self._strategy.classify_by_predicate(item, self.graph) 

546 if py_class: 

547 classified[item] = py_class 

548 except: 

549 pass 

550 

551 # ========== HELPERS & RELATED FUNCTIONS PHASE 2 ========== 

552 

553 def _apply_setters_immediate(self, instance, setters_config): 

554 """Applies static setter values from the config directly to an instance, 

555 without resolving any RDF object. Used in phase2 to apply constant values 

556 (e.g. set_is_symmetric: True) defined alongside is: class entries. 

557 """ 

558 for setter_item in setters_config: 

559 if isinstance(setter_item, dict): 

560 for setter_name, value in setter_item.items(): 

561 if hasattr(instance, setter_name): 

562 getattr(instance, setter_name)(value) 

563 else: 

564 if hasattr(instance, setter_item): 

565 getattr(instance, setter_item)() 

566 

567 # ========== HANDLERS PHASE 4 (GROUP AXIOMS) ========== 

568 

569 def process_all_disjoint_classes(self, uri: Node): 

570 """ 

571 Handles owl:AllDisjointClasses axioms by resolving the owl:members 

572 collection and marking every pair of member Concepts as mutually 

573 disjoint via set_is_disjoint_with. 

574 """ 

575 members_list = self.graph.value(uri, OWL.members) 

576 if not members_list: 

577 return 

578 try: 

579 members = list(RDFLibCollection(self.graph, members_list)) 

580 for i, class_a_uri in enumerate(members): 

581 class_a = self.get_or_create(class_a_uri, Concept) 

582 for class_b_uri in members[i + 1:]: 

583 class_b = self.get_or_create(class_b_uri, Concept) 

584 class_a.set_is_disjoint_with(class_b) 

585 class_b.set_is_disjoint_with(class_a) 

586 except Exception as e: 

587 print(f"Errore AllDisjointClasses: {e}") 

588 

589 def process_all_different(self, uri: Node): 

590 """ 

591 Handles owl:AllDifferent axioms by resolving the owl:distinctMembers 

592 collection and marking every pair of member Individuals as mutually 

593 different via set_is_different_from. 

594 """ 

595 members_list = self.graph.value(uri, OWL.distinctMembers) 

596 if not members_list: 

597 return 

598 try: 

599 members = list(RDFLibCollection(self.graph, members_list)) 

600 for i, ind_a_uri in enumerate(members): 

601 ind_a = self.get_or_create(ind_a_uri, Individual) 

602 for ind_b_uri in members[i + 1:]: 

603 ind_b = self.get_or_create(ind_b_uri, Individual) 

604 ind_a.set_is_different_from(ind_b) 

605 ind_b.set_is_different_from(ind_a) 

606 except Exception as e: 

607 print(f"Errore AllDifferent: {e}") 

608 

609 def process_all_disjoint_properties(self, uri: Node): 

610 """ 

611 Handles owl:AllDisjointProperties axioms by resolving the owl:members 

612 collection and marking every pair of member Properties as mutually 

613 disjoint via set_is_disjoint_with. 

614 """ 

615 members_list = self.graph.value(uri, OWL.members) 

616 if not members_list: 

617 return 

618 try: 

619 members = list(RDFLibCollection(self.graph, members_list)) 

620 for i, prop_a_uri in enumerate(members): 

621 prop_a = self.get_or_create(prop_a_uri, Property) 

622 for prop_b_uri in members[i + 1:]: 

623 prop_b = self.get_or_create(prop_b_uri, Property) 

624 prop_a.set_is_disjoint_with(prop_b) 

625 prop_b.set_is_disjoint_with(prop_a) 

626 except Exception as e: 

627 print(f"Errore AllDisjointProperties: {e}") 

628 

629 # ========== HANDLER CALLED BY PHASE 3 FOR POPULATING INSTANCES ========== 

630 

631 def handle_property_chain(self, instance, uri, predicate, obj, setter=None): 

632 """Handler per owl:propertyChainAxiom""" 

633 

634 if not self._is_rdf_collection(obj): 

635 raise ValueError(f"owl:propertyChainAxiom object is not an RDF list: {obj}") 

636 collection = RDFLibCollection(self.graph, obj) 

637 chain_instances = [] 

638 for chain_uri in collection: 

639 chain_instance = self.get_or_create(chain_uri, Relation) 

640 chain_instances.append(chain_instance) 

641 instance.set_has_property_chain(chain_instances) 

642 

643 def handle_has_key(self, instance, uri, predicate, obj, setter=None): 

644 try: 

645 for prop_uri in RDFLibCollection(self.graph, obj): 

646 prop = self.get_or_create(prop_uri, Property) 

647 if prop: 

648 instance.set_has_key(prop) 

649 except Exception as e: 

650 print(f"Errore handle_has_key: {e}") 

651 

652 # ========== RESTRICTIONS ================================================ 

653 

654 def handle_disjoint_union(self, instance, uri, predicate, obj, setter=None): 

655 """§9.1.4 DisjointUnion(C CE1...CEn) — equivalent to: 

656 EquivalentClasses(C ObjectUnionOf(CE1...CEn)) + DisjointClasses(CE1...CEn). 

657 """ 

658 if not isinstance(instance, Concept): 

659 return 

660 try: 

661 collection = RDFLibCollection(self.graph, obj) 

662 members = list(collection) 

663 

664 # 1. C equivalentClass ObjectUnionOf(CE1...CEn) 

665 tf = self.get_or_create(obj, TruthFunction) 

666 if tf: 

667 tf.set_has_logical_operator("or") 

668 for member_uri in members: 

669 concept = self.get_or_create(member_uri, Concept) 

670 if concept: 

671 tf.set_applies_on_concept(concept) 

672 instance.set_is_equivalent_to(tf) 

673 

674 # 2. DisjointClasses(CE1...CEn) — pairwise 

675 for i, uri_a in enumerate(members): 

676 concept_a = self.get_or_create(uri_a, Concept) 

677 for uri_b in members[i+1:]: 

678 concept_b = self.get_or_create(uri_b, Concept) 

679 if concept_a and concept_b: 

680 concept_a.set_is_disjoint_with(concept_b) 

681 concept_b.set_is_disjoint_with(concept_a) 

682 

683 except Exception as e: 

684 print(f"Errore disjointUnion: {e}") 

685 

686 def handle_datatype_restriction(self, instance, uri, predicate, obj, setter=None): 

687 on_datatype = self.graph.value(uri, OWL.onDatatype) 

688 if on_datatype: 

689 dt = self.get_or_create(on_datatype, Datatype) 

690 if dt: 

691 instance.set_applies_on_concept(dt) 

692 try: 

693 collection = RDFLibCollection(self.graph, obj) 

694 for facet_node in collection: 

695 for facet_pred, facet_val in self.graph.predicate_objects(facet_node): 

696 if facet_pred == RDF.type: 

697 continue 

698 if isinstance(facet_val, RDFlibLiteral): 

699 FACET_MAP = { 

700 str(XSD.minInclusive): ">=", 

701 str(XSD.maxInclusive): "<=", 

702 str(XSD.minExclusive): ">", 

703 str(XSD.maxExclusive): "<", 

704 str(XSD.pattern): "pattern", 

705 str(XSD.length): "length", 

706 str(XSD.minLength): "minLength", 

707 str(XSD.maxLength): "maxLength", 

708 } 

709 manchester_facet = FACET_MAP.get(str(facet_pred), str(facet_pred)) 

710 instance.set_applies_on_concept(manchester_facet) 

711 literal = self._create_literal(facet_val) 

712 instance.set_has_restriction_value(literal) 

713 

714 # Register facet node in cache keyed to the parent DatatypeRestriction 

715 # and mark all its triples as mapped, so phase6 does not create 

716 # spurious Statements or Individuals for the facet BNode. 

717 if facet_node not in self._instance_cache: 

718 self._instance_cache[facet_node] = set() 

719 self._instance_cache[facet_node].add(instance) 

720 self._track(instance, *((facet_node, fp, fv) for fp, fv in self.graph.predicate_objects(facet_node))) 

721 

722 except Exception as e: 

723 print(f"Errore handle_datatype_restriction: {e}") 

724 

725 def handle_cardinality_exactly(self, instance, uri, predicate, obj, setter=None): 

726 instance.set_has_cardinality_type("exactly") 

727 instance.set_has_cardinality(obj) 

728 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept)) 

729 

730 def handle_cardinality_min(self, instance, uri, predicate, obj, setter=None): 

731 instance.set_has_cardinality_type("min") 

732 instance.set_has_cardinality(obj) 

733 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept)) 

734 

735 def handle_cardinality_max(self, instance, uri, predicate, obj, setter=None): 

736 instance.set_has_cardinality_type("max") 

737 instance.set_has_cardinality(obj) 

738 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept)) 

739 

740 # ========== HANDLER TRUTH FUNCTIONS ========== 

741 

742 def _build_truth_function(self, instance, obj, operator): 

743 """ 

744 If instance is TruthFunction: populate in-place, return None. 

745 If instance is Concept: create a separate TruthFunction keyed on obj, return tf. 

746 

747 If the list has a single element, log a warning and: 

748 - if instance is TruthFunction: populate applies_on_concept anyway, return None 

749 - if instance is Concept: return the Concept directly (no TruthFunction created) 

750 

751 If the list is empty, log a warning and return None. 

752 """ 

753 try: 

754 items = list(RDFLibCollection(self.graph, obj)) 

755 except Exception as e: 

756 print(f"Errore build_truth_function: {e}") 

757 return None 

758 

759 if len(items) == 0: 

760 owl_warnings.empty_truth_function(self, instance, obj, operator) 

761 return None 

762 

763 if len(items) == 1: 

764 owl_warnings.singleton_truth_function(self, instance, obj, operator) 

765 single_concept = self.get_or_create(items[0], Concept) 

766 if type(instance) is TruthFunction: 

767 instance.set_has_logical_operator(operator) 

768 if single_concept: 

769 instance.set_applies_on_concept(single_concept) 

770 return None 

771 else: 

772 return single_concept 

773 

774 if type(instance) is TruthFunction: 

775 instance.set_has_logical_operator(operator) 

776 for item in items: 

777 concept = self.get_or_create(item, Concept) 

778 if concept: 

779 instance.set_applies_on_concept(concept) 

780 return None 

781 else: 

782 tf = self.get_or_create(obj, TruthFunction) 

783 if tf: 

784 tf.set_has_logical_operator(operator) 

785 for item in items: 

786 concept = self.get_or_create(item, Concept) 

787 if concept: 

788 tf.set_applies_on_concept(concept) 

789 return tf 

790 

791 def handle_intersection(self, instance, uri, predicate, obj, setter=None): 

792 if type(instance) not in (TruthFunction, Concept): 

793 return 

794 tf = self._build_truth_function(instance, obj, "and") 

795 if tf and isinstance(instance, Concept): 

796 instance.set_is_equivalent_to(tf) 

797 

798 def handle_union(self, instance, uri, predicate, obj, setter=None): 

799 if type(instance) not in (TruthFunction, Concept): 

800 return 

801 tf = self._build_truth_function(instance, obj, "or") 

802 if tf and isinstance(instance, Concept): 

803 instance.set_is_equivalent_to(tf) 

804 

805 def handle_complement(self, instance, uri, predicate, obj, setter=None): 

806 if type(instance) not in (TruthFunction, Concept): 

807 return 

808 if type(instance) is TruthFunction: 

809 instance.set_has_logical_operator("not") 

810 concept = self.get_or_create(obj, Concept) 

811 if concept: 

812 instance.set_applies_on_concept(concept) 

813 else: 

814 tf = self.get_or_create(obj, TruthFunction) 

815 if tf: 

816 tf.set_has_logical_operator("not") 

817 concept = self.get_or_create(obj, Concept) 

818 if concept: 

819 tf.set_applies_on_concept(concept) 

820 instance.set_is_equivalent_to(tf) 

821 

822 def handle_one_of(self, instance, uri, predicate, obj, setter=None): 

823 if type(instance) not in (OneOf, Concept): 

824 return 

825 if type(instance) is Concept: 

826 one_of = self.get_or_create(uri, OneOf) 

827 if one_of: 

828 try: 

829 collection = RDFLibCollection(self.graph, obj) 

830 for item in collection: 

831 resource = self.get_or_create(item, Individual) 

832 if resource: 

833 one_of.set_applies_on_resource(resource) 

834 except Exception as e: 

835 print(f"Errore oneOf su Concept: {e}") 

836 instance.set_is_equivalent_to(one_of) 

837 else: # type(instance) is OneOf 

838 try: 

839 collection = RDFLibCollection(self.graph, obj) 

840 for item in collection: 

841 resource = self.get_or_create(item, Individual) 

842 if resource: 

843 instance.set_applies_on_resource(resource) 

844 except Exception as e: 

845 print(f"Errore oneOf: {e}") 

846 

847 def handle_has_value(self, instance, uri, predicate, obj, setter=None): 

848 resource = self.get_or_create(obj, Resource) 

849 if resource: 

850 instance.set_applies_on_resource(resource) 

851 on_prop = self.graph.value(uri, OWL.onProperty) 

852 if on_prop: 

853 prop = self.get_or_create(on_prop, Property) 

854 if prop: 

855 instance.set_applies_on_property(prop) 

856 

857 

858 def handle_rdf_type(self, instance, uri, predicate, obj, setter=None): 

859 """ 

860 Handler bidirezionale per rdf:type. 

861 

862 - subject.has_type += object (lato Individual/Resource) 

863 - object.individuals_with_this_type += subject (lato Concept, solo se l'oggetto e' 

864 effettivamente classificato come Concept e non e' un URI strutturale 

865 del vocabolario) 

866 

867 Replica la filter logic di _apply_setters per set_has_type: scarta 

868 oggetti nei namespace OWL/RDF/RDFS, eccetto owl:Thing, owl:Nothing, 

869 rdfs:Literal. 

870 """ 

871 if not isinstance(obj, URIRef) and not isinstance(obj, BNode): 

872 return 

873 

874 # Filtro URI strutturali (stessa logica di _apply_setters) 

875 if isinstance(obj, URIRef): 

876 obj_str = str(obj) 

877 if any(obj_str.startswith(ns) for ns in self._allowed_namespaces) \ 

878 and obj not in (OWL.Thing, OWL.Nothing, RDFS.Literal): 

879 return 

880 

881 concept = self.get_or_create(obj, Concept) 

882 if concept is None: 

883 return 

884 

885 # Lato diretto 

886 if hasattr(instance, 'set_has_type'): 

887 instance.set_has_type(concept) 

888 

889 # Lato inverso: solo se l'oggetto e' realmente un Concept 

890 # (get_or_create puo' promuovere; type() esatto evita di sporcare 

891 # TruthFunction/Restriction/OneOf/etc.) 

892 if type(concept) is Concept and isinstance(instance, Individual): 

893 concept.set_individuals_with_this_type(instance) 

894 

895 

896 # ========== OVERRIDE Statement per OWL (typed subject/object) ========== 

897 

898 def _create_statement_for_triple(self, subj, pred, obj): 

899 """ 

900 Creates a Statement instance for a triple that was not mapped during 

901 population phases. 

902 

903 Skips rdf:type triples — those are handled by phase2 and must not be 

904 reified as Statements. 

905 

906 Subject is resolved as Individual; if the namespace filter blocks it 

907 (e.g. OWL/RDF/RDFS URIs), the triple is silently dropped. 

908 

909 Predicate is looked up in the instance cache first. If not found, a bare 

910 Annotation is created directly, bypassing the namespace filter — predicates 

911 must always be reifiable regardless of their namespace. 

912 

913 Object is resolved based on its RDF type: RDF collections become 

914 Containers, RDFLib Literals become Literal instances, everything else 

915 is resolved as Individual. 

916 

917 OWL defaults (owl:Thing type for untyped Individuals) are applied to 

918 subject and object after the Statement is built, since these instances 

919 may have been created here for the first time, after phase3 defaults 

920 have already run. 

921 """ 

922 if pred == RDF.type or pred in self._property_mapping: 

923 return 

924 

925 # Subject: an unmapped structured BNode is a Statement, not an Individual. 

926 # This handles the case where phase6 visits the BNode's outgoing triples 

927 # before visiting the triple that points to it. 

928 if self._is_unmapped_structured_bnode(subj): 

929 # Just materialise it; its outgoing triples will be absorbed inside. 

930 # Skip emitting a wrapper Statement for this triple — _create_nested_statement 

931 # already records (subj, pred, obj) in the BNode-Statement's triples_map. 

932 self._create_nested_statement(subj) 

933 return 

934 

935 subj_inst = self._resolve_statement_endpoint(subj, Individual) 

936 if subj_inst is None: 

937 # Fallback for structural-namespace subjects: create a bare Resource 

938 # so the predicate is still reifiable. 

939 subj_inst = Resource() 

940 subj_inst.set_has_identifier(str(subj)) 

941 if subj not in self._instance_cache: 

942 self._instance_cache[subj] = set() 

943 self._instance_cache[subj].add(subj_inst) 

944 

945 if pred in self._instance_cache: 

946 pred_inst = next( 

947 (i for i in self._instance_cache[pred] if isinstance(i, Property)), 

948 None 

949 ) 

950 else: 

951 pred_inst = None 

952 

953 if pred_inst is None: 

954 pred_inst = Annotation() 

955 pred_inst.set_has_identifier(str(pred)) 

956 if pred not in self._instance_cache: 

957 self._instance_cache[pred] = set() 

958 self._instance_cache[pred].add(pred_inst) 

959 

960 statement = Statement() 

961 stmt_bnode = BNode() 

962 statement.set_has_identifier(str(stmt_bnode)) 

963 

964 self._track(statement, (subj, pred, obj)) 

965 

966 statement.set_has_subject(subj_inst) 

967 statement.set_has_predicate(pred_inst) 

968 

969 if self._is_rdf_collection(obj): 

970 obj_inst = self._convert_collection_to_container(obj) 

971 elif isinstance(obj, RDFlibLiteral): 

972 obj_inst = self._create_literal(obj) 

973 elif self._is_unmapped_structured_bnode(obj): 

974 obj_inst = self._create_nested_statement(obj) 

975 else: 

976 # oggetto mai visto: Individual (fallback class — _resolve_statement_endpoint 

977 # ritorna il dominante in cache se presente, altrimenti crea Individual) 

978 obj_inst = self._resolve_statement_endpoint(obj, Resource) 

979 

980 if obj_inst: 

981 statement.set_has_object(obj_inst) 

982 

983 if stmt_bnode not in self._instance_cache: 

984 self._instance_cache[stmt_bnode] = set() 

985 self._instance_cache[stmt_bnode].add(statement) 

986 

987 self._enrich_or_apply_owl_defaults(subj_inst, subj) 

988 if obj_inst and not isinstance(obj, RDFlibLiteral): 

989 self._enrich_or_apply_owl_defaults(obj_inst, obj) 

990 

991 def _resolve_statement_endpoint(self, node, fallback_class): 

992 dom = self._get_punning_dominant(node) 

993 if dom is not None: 

994 return dom 

995 return self.get_or_create(node, fallback_class) 

996 

997 # ========== PROVENANCE OWL-SPECIFIC ========== 

998 

999 _PROVENANCE_AXIOM_TYPES = ( 

1000 OWL.AllDisjointClasses, 

1001 OWL.AllDifferent, 

1002 OWL.AllDisjointProperties, 

1003 ) 

1004 

1005 def _add_axiom_provenance(self, instance, sub): 

1006 """Include general axioms that mention `instance` URI in members/distinctMembers.""" 

1007 uri_str = getattr(instance, 'has_identifier', None) 

1008 if not uri_str: 

1009 return 

1010 uri_ref = URIRef(uri_str) 

1011 for axiom_type in self._PROVENANCE_AXIOM_TYPES: 

1012 for axiom in self.graph.subjects(RDF.type, axiom_type): 

1013 if self._uri_in_axiom_members(axiom, uri_ref): 

1014 self._expand_bnode_into(axiom, sub, set()) 

1015 

1016 def _uri_in_axiom_members(self, axiom_node, uri_ref): 

1017 """True if uri_ref appears in owl:members or owl:distinctMembers list of axiom_node.""" 

1018 for list_pred in (OWL.members, OWL.distinctMembers): 

1019 head = self.graph.value(axiom_node, list_pred) 

1020 if head is None: 

1021 continue 

1022 node = head 

1023 while node and node != RDF.nil: 

1024 first = self.graph.value(node, RDF.first) 

1025 if first == uri_ref: 

1026 return True 

1027 node = self.graph.value(node, RDF.rest) 

1028 return False