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

371 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-03-25 15:05 +0000

1# owl_logic.py 

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

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

4from rdflib.collection import Collection as RDFLibCollection 

5 

6from lode.models import * 

7from lode.reader.logic.base_logic import BaseLogic 

8 

9 

10class OwlLogic(BaseLogic): 

11 """ 

12 OWL-specific parsing logic. 

13 

14 Extends BaseLogic with: 

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

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

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

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

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

20 """ 

21 

22 # _get_allowed_namespaces: ereditato da BaseLogic, legge config YAML 

23 

24 # ========== HOOK per resolve custom OWL ========== 

25 

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

27 """ 

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

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

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

31 """ 

32 if id and id in self._instance_cache: 

33 for existing in self._instance_cache[id]: 

34 if type(existing) is not Individual: 

35 return type(existing) 

36 return None 

37 

38 # ========== READER PHASES ========== 

39 

40 def phase1_classify_from_predicates(self): 

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

42 - BNodes with inferred_class predicates -> classified and registered 

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

44 the inferred type from classify_by_predicate (only if unambiguous) 

45 """ 

46 

47 classified = {} 

48 

49 # Iterate ALL mapped predicates 

50 for pred in self._property_mapping: 

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

52 if uri in self._instance_cache: 

53 continue 

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

55 if not python_class: 

56 continue 

57 # Goes for restrictions 

58 if isinstance(uri, BNode): 

59 if uri not in classified: 

60 classified[uri] = python_class 

61 # Goes for any other URI 

62 elif isinstance(uri, URIRef): 

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

64 

65 # classify recursively restrictions 

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

67 

68 # creates classified restrictions after recursion 

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

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

71 

72 def phase2_create_from_types(self): 

73 """Creates instances from rdf:type triples mapped in the config (is: class). 

74 For each mapped rdf:type, uses the target_class field to resolve the 

75 Python class to instantiate, then applies any immediate static setters defined in the config entry. Subjects whose rdf:type is not covered by the config are instantiated as Individual, provided they are URIRefs not yet in the cache.""" 

76 

77 type_mapping = self._strategy.get_type_mapping() 

78 

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

80 py_class = config.get('target_class') 

81 if not py_class: 

82 continue 

83 

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

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

86 

87 # apply setters immediately 

88 if 'setters' in config: 

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

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

91 

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

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

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

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

96 

97 def phase3_populate_properties(self): 

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

99 by dispatching each predicate-object pair through the configured 

100 setters and handlers defined in the property mapping. 

101 """ 

102 

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

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

105 self.populate_instance(instance, uri) 

106 

107 def phase4_process_group_axioms(self): 

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

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

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

111 """ 

112 

113 axioms = self._strategy.get_group_axioms() 

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

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

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

117 getattr(self, handler_name)(uri) 

118 

119 def phase5_fallback(self): 

120 """ 

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

122 determined in earlier phases, in particular generic Property instances 

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

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

125 

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

127 """ 

128 

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

130 for instance in list(instances): 

131 

132 if type(instance) is Property: 

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

134 

135 has_concrete = any( 

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

137 for i in instances if i is not instance 

138 ) 

139 

140 if has_concrete: 

141 # Remove generic Property, keep the concrete one 

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

143 else: 

144 # No concrete type found — infer and reclassify 

145 inferred = self._infer_property_type(instance) 

146 new = inferred() 

147 new.__dict__.update(instance.__dict__) 

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

149 self._instance_cache[uri].add(new) 

150 if instance in self._triples_map: 

151 self._triples_map[new] = self._triples_map.pop(instance) 

152 self.populate_instance(new, uri) 

153 

154 self._enrich_or_apply_owl_defaults(instance, uri) 

155 

156 if instance.__class__ == DatatypeRestriction: 

157 print('DEBUG::', vars(instance)) 

158 for constraint, value in zip(instance.get_has_constraint(), instance.get_has_restriction_value()): 

159 print(f" constraint: {constraint.get_has_identifier()}, value: {value.get_has_value()}") 

160 

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

162 """ 

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

164 generic Property instance (not better classified) 

165 

166 Strategy (in order): 

167 1. Traverse UP the subPropertyOf chain: if any ancestor has a concrete 

168 type, inherit it (if A subPropertyOf B and B is Relation, A is Relation). 

169 2. Traverse DOWN by scanning the cache for properties that declare this 

170 instance as their superproperty: if any subproperty has a concrete 

171 type, inherit it (if B is Relation and B subPropertyOf A, A is Relation). 

172 3. Fall back to Annotation if no type can be inferred from the hierarchy. 

173 Annotation makes no domain/range assumptions and is valid for any 

174 subject/object combination. 

175 """ 

176 

177 visited = set() 

178 queue = [instance] 

179 

180 # (1) Goes up and down subproperty hierarchy 

181 # (1.1) Traverse UP superproperties 

182 while queue: 

183 current = queue.pop(0) 

184 if id(current) in visited: 

185 continue 

186 visited.add(id(current)) 

187 

188 if type(current) in (Relation, Attribute, Annotation): 

189 return type(current) 

190 

191 for sup in (current.get_is_sub_property_of() or []): 

192 queue.append(sup) 

193 

194 # (1.2) Traverse DOWN subproperties 

195 for instances_set in self._instance_cache.values(): 

196 for inst in instances_set: 

197 if isinstance(inst, Property): 

198 for sup in (inst.get_is_sub_property_of() or []): 

199 if sup is instance: 

200 t = self._infer_property_type(inst) 

201 if t in (Relation, Attribute, Annotation): 

202 return t 

203 

204 # (2) fallback  

205 return Annotation 

206 

207 def _enrich_or_apply_owl_defaults(self, instance, uri): 

208 """ 

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

210 

211 OWL open-world assumption requires every property to have a domain and 

212 range. These are resolved first by traversing the rdfs:subPropertyOf 

213 chain upward — if an ancestor declares domain/range, those are inherited. 

214 Only if no value is found anywhere in the chain does the default apply. 

215 

216 Relation (owl:ObjectProperty): 

217 domain -> owl:Thing (applies to any individual) 

218 range -> owl:Thing (returns any individual) 

219 

220 Attribute (owl:DatatypeProperty): 

221 domain -> owl:Thing (applies to any individual) 

222 range -> rdfs:Literal (returns any literal value) 

223 

224 Individual: 

225 If no rdf:type is declared, defaults to owl:Thing — the individual 

226 exists but its class is unknown. 

227 """ 

228 

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

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

231 

232 if isinstance(instance, Relation): 

233 inherited_domain = self._get_inherited_property_values(instance, "get_has_domain") 

234 if len(inherited_domain) == 0: 

235 instance.set_has_domain(owl_thing) 

236 else: 

237 for domain in inherited_domain: 

238 instance.set_has_domain(domain) 

239 

240 inherited_range = self._get_inherited_property_values(instance, "get_has_range") 

241 if len(inherited_range) == 0: 

242 instance.set_has_range(owl_thing) 

243 else: 

244 for range in inherited_range: 

245 instance.set_has_range(range) 

246 

247 if isinstance(instance, Attribute): 

248 

249 inherited_domain = self._get_inherited_property_values(instance, "get_has_domain") 

250 if len(inherited_domain) == 0: 

251 instance.set_has_domain(owl_thing) 

252 else: 

253 for domain in inherited_domain: 

254 instance.set_has_domain(domain) 

255 

256 inherited_range = self._get_inherited_property_values(instance, "get_has_range") 

257 if len(inherited_range) == 0: 

258 instance.set_has_range(rdfs_string) 

259 else: 

260 for range in inherited_range: 

261 instance.set_has_range(range) 

262 

263 # Default type per Individual 

264 if isinstance(instance, Individual): 

265 if not instance.get_has_type(): 

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

267 instance.set_has_type(owl_thing) 

268 

269 

270 if isinstance(instance, Resource): 

271 # adds to Resources involved in punning the also defined as 

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

273 for other in self._instance_cache[uri]: 

274 if other is not instance: 

275 instance.set_also_defined_as(other) 

276 

277 

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

279 """ 

280 Traversal upward along rdfs:subPropertyOf looking for values 

281 exposed by getter_name (e.g. get_has_domain, get_has_range). 

282 Returns list of values, or [] if none found in the chain. 

283 """ 

284 def collect(node): 

285 getter = getattr(node, getter_name, None) 

286 if getter: 

287 values = getter() 

288 if values: 

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

290 return None 

291 

292 # (1) If this is a Relation with an inverse, check swapped domain/range on the inverse 

293 if isinstance(property_instance, Relation): 

294 inverse = property_instance.get_is_inverse_of() 

295 if inverse: 

296 swapped_getter = { 

297 "get_has_domain": "get_has_range", 

298 "get_has_range": "get_has_domain", 

299 }.get(getter_name) 

300 if swapped_getter: 

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

302 if swapped: 

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

304 

305 # Traverses the hierarchy up to inherit domain and ranges  

306 result = self._traverse_hierarchy( 

307 property_instance, 

308 next_getter="get_is_sub_property_of", 

309 direction="up", 

310 collect=collect, 

311 ) 

312 return result if result is not None else [] 

313 

314 

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

316 """ 

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

318 generic Property instance. 

319 

320 Strategy: 

321 1. owl:inverseOf — if inverse is a Relation, this is a Relation. 

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

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

324 4. Fallback to Annotation. 

325 """ 

326 # (1) inverseOf 

327 inverse = getattr(instance, 'get_is_inverse_of', lambda: None)() 

328 if inverse and type(inverse) is Relation: 

329 return Relation 

330 

331 CONCRETE = (Relation, Attribute, Annotation) 

332 

333 # (2) UP THE HIERARCHY 

334 def collect_type(node): 

335 if type(node) in CONCRETE: 

336 return type(node) 

337 return None 

338 

339 result = self._traverse_hierarchy( 

340 instance, 

341 next_getter="get_is_sub_property_of", 

342 direction="up", 

343 collect=collect_type, 

344 ) 

345 if result: 

346 return result 

347 

348 # (3) DOWN THE HIERARCHY 

349 result = self._traverse_hierarchy( 

350 instance, 

351 next_getter="get_is_sub_property_of", 

352 direction="down", 

353 collect=collect_type, 

354 ) 

355 if result: 

356 return result 

357 

358 # (4) fallback 

359 return Annotation 

360 

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

362 

363 def _classify_nested(self, classified, predicates): 

364 """ 

365 Recursively classifies BNodes found inside OWL list collections 

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

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

368 BNode-to-class mappings. 

369 """ 

370 

371 list_preds = [OWL.intersectionOf, OWL.unionOf, OWL.oneOf] 

372 

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

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

375 if pred in list_preds: 

376 try: 

377 collection = RDFLibCollection(self.graph, obj) 

378 for item in collection: 

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

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

381 if py_class: 

382 classified[item] = py_class 

383 except: 

384 pass 

385 

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

387 

388 def _apply_setters_immediate(self, instance, setters_config): 

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

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

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

392 """ 

393 for setter_item in setters_config: 

394 if isinstance(setter_item, dict): 

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

396 if hasattr(instance, setter_name): 

397 getattr(instance, setter_name)(value) 

398 else: 

399 if hasattr(instance, setter_item): 

400 getattr(instance, setter_item)() 

401 

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

403 

404 def process_all_disjoint_classes(self, uri: Node): 

405 """ 

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

407 collection and marking every pair of member Concepts as mutually 

408 disjoint via set_is_disjoint_with. 

409 """ 

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

411 if not members_list: 

412 return 

413 try: 

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

415 for i, class_a_uri in enumerate(members): 

416 class_a = self.get_or_create(class_a_uri, Concept) 

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

418 class_b = self.get_or_create(class_b_uri, Concept) 

419 class_a.set_is_disjoint_with(class_b) 

420 class_b.set_is_disjoint_with(class_a) 

421 except Exception as e: 

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

423 

424 def process_all_different(self, uri: Node): 

425 """ 

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

427 collection and marking every pair of member Individuals as mutually 

428 different via set_is_different_from. 

429 """ 

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

431 if not members_list: 

432 return 

433 try: 

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

435 for i, ind_a_uri in enumerate(members): 

436 ind_a = self.get_or_create(ind_a_uri, Individual) 

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

438 ind_b = self.get_or_create(ind_b_uri, Individual) 

439 ind_a.set_is_different_from(ind_b) 

440 ind_b.set_is_different_from(ind_a) 

441 except Exception as e: 

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

443 

444 def process_all_disjoint_properties(self, uri: Node): 

445 """ 

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

447 collection and marking every pair of member Properties as mutually 

448 disjoint via set_is_disjoint_with. 

449 """ 

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

451 if not members_list: 

452 return 

453 try: 

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

455 for i, prop_a_uri in enumerate(members): 

456 prop_a = self.get_or_create(prop_a_uri, Property) 

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

458 prop_b = self.get_or_create(prop_b_uri, Property) 

459 prop_a.set_is_disjoint_with(prop_b) 

460 prop_b.set_is_disjoint_with(prop_a) 

461 except Exception as e: 

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

463 

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

465 

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

467 try: 

468 collection = RDFLibCollection(self.graph, obj) 

469 chain = [self.get_or_create(chain_uri, Relation) for chain_uri in collection] 

470 instance.set_has_property_chain(chain) 

471 except Exception as e: 

472 print(f"Errore propertyChain: {e}") 

473 

474 # ========== RESTRICTIONS ================================================ 

475 

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

477 try: 

478 collection = RDFLibCollection(self.graph, obj) 

479 for facet_node in collection: 

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

481 if isinstance(facet_val, RDFlibLiteral): 

482 annotation = self.get_or_create(facet_pred, Annotation) 

483 literal = self._create_literal(facet_val) 

484 instance.set_has_constraint(annotation) 

485 instance.set_has_restriction_value(literal) 

486 except Exception as e: 

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

488 

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

490 instance.set_has_cardinality_type("exactly") 

491 instance.set_has_cardinality(obj) 

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

493 

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

495 instance.set_has_cardinality_type("min") 

496 instance.set_has_cardinality(obj) 

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

498 

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

500 instance.set_has_cardinality_type("max") 

501 instance.set_has_cardinality(obj) 

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

503 

504 # ========== HANDLER TRUTH FUNCTIONS ========== 

505 

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

507 """ 

508 Se instance e' TruthFunction: popola in-place, ritorna None. 

509 Se instance e' Concept: crea TruthFunction separata keyed su obj, ritorna tf. 

510 """ 

511 if type(instance) is TruthFunction: 

512 instance.set_has_logical_operator(operator) 

513 try: 

514 for item in RDFLibCollection(self.graph, obj): 

515 concept = self.get_or_create(item, Concept) 

516 if concept: 

517 instance.set_applies_on_concept(concept) 

518 except Exception as e: 

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

520 return None 

521 else: 

522 tf = self.get_or_create(obj, TruthFunction) 

523 if tf: 

524 tf.set_has_logical_operator(operator) 

525 try: 

526 for item in RDFLibCollection(self.graph, obj): 

527 concept = self.get_or_create(item, Concept) 

528 if concept: 

529 tf.set_applies_on_concept(concept) 

530 except Exception as e: 

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

532 return tf 

533 

534 # HANDLER FOR RELATION INVERSE OF, INFERS THE INVERSE - domains and ranges are inferred in phase 5 

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

536 

537 instance = self.get_or_create(uri, Relation) 

538 inverse = self.get_or_create(obj, Relation) 

539 instance.set_is_inverse_of(inverse) 

540 inverse.set_is_inverse_of(instance) 

541 

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

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

544 return 

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

546 if tf and isinstance(instance, Concept): 

547 instance.set_is_equivalent_to(tf) 

548 

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

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

551 return 

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

553 if tf and isinstance(instance, Concept): 

554 instance.set_is_equivalent_to(tf) 

555 

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

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

558 return 

559 if type(instance) is TruthFunction: 

560 instance.set_has_logical_operator("not") 

561 concept = self.get_or_create(obj, Concept) 

562 if concept: 

563 instance.set_applies_on_concept(concept) 

564 else: 

565 tf = self.get_or_create(obj, TruthFunction) 

566 if tf: 

567 tf.set_has_logical_operator("not") 

568 concept = self.get_or_create(obj, Concept) 

569 if concept: 

570 tf.set_applies_on_concept(concept) 

571 instance.set_is_equivalent_to(tf) 

572 

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

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

575 return 

576 if isinstance(instance, Concept): 

577 one_of = self.get_or_create(uri, OneOf) 

578 if one_of: 

579 try: 

580 for item in RDFLibCollection(self.graph, obj): 

581 resource = self.get_or_create(item, Individual) 

582 if resource: 

583 one_of.set_applies_on_resource(resource) 

584 except Exception as e: 

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

586 instance.set_is_equivalent_to(one_of) 

587 else: 

588 try: 

589 for item in RDFLibCollection(self.graph, obj): 

590 resource = self.get_or_create(item, Individual) 

591 if resource: 

592 instance.set_applies_on_resource(resource) 

593 except Exception as e: 

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

595 

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

597 

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

599 """OWL override: soggetto -> Individual, oggetto tipizzato.""" 

600 statement = Statement() 

601 stmt_bnode = BNode() 

602 statement.set_has_identifier(str(stmt_bnode)) 

603 

604 if statement not in self._triples_map: 

605 self._triples_map[statement] = set() 

606 self._triples_map[statement].add((subj, pred, obj)) 

607 

608 if pred != RDF.type: 

609 # if the subject of a Statement is a BNode, then its reification (they can be shacl shapes :)) 

610 if isinstance(subj, BNode): 

611 subj_inst = self.get_or_create(subj, Statement) 

612 # otherwise its most likely a Named Individual (get_or_create decides the actual class) 

613 else: 

614 subj_inst = self.get_or_create(subj, Individual) 

615 

616 statement.set_has_subject(subj_inst) 

617 

618 if self._is_rdf_collection(obj): 

619 obj_inst = self._convert_collection_to_container(obj) 

620 pred_inst = self.get_or_create(pred, Relation) 

621 elif isinstance(obj, RDFlibLiteral): 

622 obj_inst = self._create_literal(obj) 

623 # if the predicate is not in cache creates an annotation 

624 if pred not in self._instance_cache: 

625 pred_inst = self.get_or_create(pred, Annotation) 

626 # otherwise reuses the existing predicate 

627 else: 

628 pred_inst = self.get_or_create(pred, Property) 

629 elif isinstance(obj, BNode): 

630 # BNode object = reified annotation, use existing instance or create new Statement 

631 obj_inst = self.get_or_create(obj, Statement) 

632 # if the predicate is not in cache creates an annotation 

633 if pred not in self._instance_cache: 

634 pred_inst = self.get_or_create(pred, Annotation) 

635 # otherwise reuses the existing predicate 

636 else: 

637 pred_inst = self.get_or_create(pred, Property) 

638 

639 else: 

640 obj_inst = self.get_or_create(obj, Resource) 

641 # if the predicate is not in cache creates an annotation 

642 if pred not in self._instance_cache: 

643 pred_inst = self.get_or_create(pred, Annotation) 

644 # otherwise reuses the existing predicate 

645 else: 

646 pred_inst = self.get_or_create(pred, Property) 

647 

648 # pred or obj may be None if their URI is in a protected namespace (e.g. rdf:, rdfs:) 

649 if pred_inst is None: 

650 pred_inst = Annotation() 

651 pred_inst.set_has_identifier(str(pred)) 

652 

653 if obj_inst is None: 

654 obj_inst = Resource() 

655 obj_inst.set_has_identifier(str(obj)) 

656 

657 statement.set_has_predicate(pred_inst) 

658 statement.set_has_object(obj_inst) 

659 

660 if stmt_bnode not in self._instance_cache: 

661 self._instance_cache[stmt_bnode] = set() 

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