Coverage for lode / reader / warnings / owl_warnings.py: 77%
92 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# owl_warnings.py
2"""OWL warning checks. All functions take `logic` as first arg and push via logic.add_warning()."""
4from rdflib import URIRef, BNode
5from rdflib.namespace import OWL
6from lode.models import (
7 Concept, Datatype, DatatypeRestriction,
8 Relation, Attribute, Annotation,
9 TruthFunction, PropertyConceptRestriction,
10)
13# for integrity tests
14def has_warning(logic, code: str, subject=None) -> bool:
15 """Check if a warning with given code (and optional subject) was raised."""
16 for w in logic._warnings:
17 if w['code'] != code:
18 continue
19 if subject is None or w['subject'] == str(subject):
20 return True
21 return False
23# ====================================================================
24# PIPELINE-TIME (called inline from owl_logic during specific phases)
25# ====================================================================
27def flag_malformed_restrictions(logic, classified):
28 axiom_preds = {
29 p for p, cfg in logic._property_mapping.items()
30 if isinstance(cfg.get('inferred_class'), type)
31 and issubclass(cfg['inferred_class'], PropertyConceptRestriction)
32 }
33 by_parent = {} # parent_uri -> set of axiom predicates seen
34 for bnode in classified:
35 axioms = set(logic.graph.predicates(subject=bnode)) & axiom_preds
36 if len(axioms) <= 1:
37 continue
38 parents = sorted({
39 str(s) for s in logic.graph.subjects(object=bnode)
40 if isinstance(s, URIRef)
41 })
42 parent_key = parents[0] if parents else '<anonymous>'
43 by_parent.setdefault(parent_key, set()).update(axioms)
45 for parent, axioms in by_parent.items():
46 names = sorted(p.n3(logic.graph.namespace_manager) for p in axioms)
47 logic.add_warning('malformed_restriction', parent,
48 f"OWL 2 DL: malformed restrictions on {parent} ({', '.join(names)})")
50def empty_truth_function(logic, instance, bnode, operator):
51 ident = instance.get_has_identifier()
52 logic.add_warning('empty_truth_function', ident,
53 f'§8.1.1: Truth Function ({ident}, {operator}) is empty — ignored (collection BNode: {bnode})')
55def singleton_truth_function(logic, instance, bnode, operator):
56 ident = instance.get_has_identifier()
57 logic.add_warning('singleton_truth_function', ident,
58 f'§8.1.1: TruthFunction ({ident}, {operator}) has a single element — ontology should be checked (collection BNode: {bnode})')
61# ====================================================================
62# POST-PIPELINE (single entry point)
63# ====================================================================
65def run_post_checks(logic):
66 """Single sweep over cache + config-driven sweep."""
67 for uri, instances in list(logic._instance_cache.items()):
68 for instance in list(instances):
69 _check_orphan_restriction(logic, instance, uri)
70 _check_inverse_multiplicity(logic, instance, uri)
71 # _check_self_inverse(logic, instance, uri)
72 _check_empty_truth_function_post(logic, instance, uri)
73 _check_property_hierarchy(logic, instance, uri)
74 _check_class_datatype_hierarchy(logic, instance, uri)
75 if isinstance(uri, URIRef):
76 _check_punning_conflicts(logic, uri, instances)
77 _check_config_mismatches(logic)
80def _check_orphan_restriction(logic, instance, uri):
81 if not isinstance(instance, PropertyConceptRestriction):
82 return
83 if instance.get_applies_on_property() is not None:
84 return
85 if isinstance(uri, BNode) and (uri, OWL.onProperty, None) not in logic.graph:
86 logic.add_warning('orphan_restriction', uri,
87 f'Orphan {type(instance).__name__}: no owl:onProperty in graph, removed')
88 logic._instance_cache[uri].discard(instance)
89 else:
90 logic.add_warning('missing_on_property', uri,
91 f'{type(instance).__name__} has owl:onProperty but applies_on_property is None')
93def _check_inverse_multiplicity(logic, instance, uri):
94 if not isinstance(instance, Relation):
95 return
96 inv = instance.get_is_inverse_of()
97 if isinstance(inv, list) and len(inv) > 1:
98 ids = ', '.join(i.get_has_identifier() for i in inv)
99 logic.add_warning('multiple_inverse_of', uri,
100 f'§9.2.4: Relation {uri} has {len(inv)} owl:inverseOf declarations ({ids})')
102# def _check_self_inverse(logic, instance, uri):
103# if not isinstance(instance, Relation):
104# return
105# inv = instance.get_is_inverse_of()
106# candidates = inv if isinstance(inv, list) else ([inv] if inv else [])
107# if any(c is instance for c in candidates):
108# logic.add_warning('self_inverse', uri,
109# f'§6.1.1: Relation {uri} is inverse of itself')
111def _check_empty_truth_function_post(logic, instance, uri):
112 if isinstance(instance, TruthFunction) and len(instance.get_applies_on_concept()) == 0:
113 logic.add_warning('empty_restriction', uri,
114 f'{type(instance).__name__} has empty applies_on_concept')
116def _check_property_hierarchy(logic, instance, uri):
117 if not isinstance(instance, (Relation, Attribute, Annotation)):
118 return
119 for sup in (instance.get_is_sub_property_of() or []):
120 if sup is not None and not isinstance(sup, type(instance)):
121 logic.add_warning('property_type_mismatch_hierarchy', uri,
122 f'§5.8.1: {type(instance).__name__} {uri} subPropertyOf '
123 f'{type(sup).__name__} {sup.get_has_identifier()}')
125def _check_class_datatype_hierarchy(logic, instance, uri):
126 if not isinstance(instance, (Concept, Datatype)) or isinstance(instance, DatatypeRestriction):
127 return
128 for sup in (instance.get_is_sub_concept_of() or []):
129 if sup is not None and not isinstance(sup, type(instance)):
130 logic.add_warning('class_datatype_mismatch_hierarchy', uri,
131 f'§5.8.1: {type(instance).__name__} {uri} subClassOf '
132 f'{type(sup).__name__} {sup.get_has_identifier()}')
134def _check_punning_conflicts(logic, uri, instances):
135 types = {type(i) for i in instances}
136 if Concept in types and Datatype in types:
137 logic.add_warning('class_datatype_conflict', uri,
138 f'§5.8.1: {uri} has both Concept and Datatype in cache')
139 prop_types = types & {Relation, Attribute, Annotation}
140 if len(prop_types) > 1:
141 logic.add_warning('property_type_conflict', uri,
142 f'§5.8.1: {uri} has multiple property types: {sorted(t.__name__ for t in prop_types)}')
144def _check_config_mismatches(logic):
145 for pred, cfg in logic._property_mapping.items():
146 target_classes = cfg.get('target_classes', [])
147 if not target_classes:
148 continue
149 target_tuple = tuple(target_classes)
150 expected = [c.__name__ for c in target_classes]
151 for uri in logic.graph.subjects(pred, None):
152 if not isinstance(uri, URIRef):
153 continue
154 instances = logic._instance_cache.get(uri, set())
155 if instances and not any(isinstance(i, target_tuple) for i in instances):
156 actual = [type(i).__name__ for i in instances]
157 logic.add_warning('config_type_mismatch', uri,
158 f'{uri} subject of {pred} (expected {expected}) got {actual}')