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

1# owl_warnings.py 

2"""OWL warning checks. All functions take `logic` as first arg and push via logic.add_warning().""" 

3 

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) 

11 

12 

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 

22 

23# ==================================================================== 

24# PIPELINE-TIME (called inline from owl_logic during specific phases) 

25# ==================================================================== 

26 

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) 

44 

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

49 

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})') 

54 

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})') 

59 

60 

61# ==================================================================== 

62# POST-PIPELINE (single entry point) 

63# ==================================================================== 

64 

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) 

78 

79 

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') 

92 

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})') 

101 

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') 

110 

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') 

115 

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()}') 

124 

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()}') 

133 

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)}') 

143 

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}')