Skip to content

Commit bcb5cac

Browse files
committed
fix(shacl): 🐛 evaluate inherited shapes for zero-shape target profiles
Override `SHACLRequirement.finalize` to force one pyshacl run on the merged shapes graph when the target profile contributes no SHACL checks of its own (e.g. extension profiles that purely inherit or only deactivate). Without it, the main loop never triggers a SHACL run and inherited shapes are silently skipped.
1 parent 439c68f commit bcb5cac

1 file changed

Lines changed: 67 additions & 3 deletions

File tree

rocrate_validator/requirements/shacl/requirements.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@
1919

2020
from rocrate_validator.utils import log as logging
2121
from rocrate_validator.constants import VALIDATOR_NS
22-
from rocrate_validator.models import (Profile, Requirement, RequirementCheck,
23-
RequirementLevel, RequirementLoader)
22+
from rocrate_validator.models import (
23+
Profile,
24+
Requirement,
25+
RequirementCheck,
26+
RequirementLevel,
27+
RequirementLoader,
28+
ValidationContext,
29+
)
2430
from rocrate_validator.requirements.shacl.checks import SHACLCheck
2531
from rocrate_validator.requirements.shacl.models import Shape, ShapesRegistry
2632

@@ -82,9 +88,67 @@ def hidden(self) -> bool:
8288
return True
8389
return False
8490

91+
@classmethod
92+
def finalize(cls, context: ValidationContext) -> None:
93+
""" "
94+
Finalize the SHACL requirement by ensuring that a SHACL validation run is triggered for the target profile
95+
if it has no shapes of its own (e.g. an extension profile that purely inherits or only deactivates).
96+
97+
SHACL is normally driven by the first execute_check of a check
98+
belonging to the target profile (see SHACLValidationContextManager).
99+
If the target has zero SHACL checks of its own (e.g. an extension
100+
profile that purely inherits or only deactivates), no pyshacl run
101+
is ever triggered and inherited shapes are never evaluated.
102+
Force one final run on the merged shapes graph in that case.
103+
"""
104+
105+
logger.debug("Starting %s requirement finalization for context %s", cls.__name__, context)
106+
107+
# extract profiles and target profile from context
108+
profiles = context.profiles
109+
110+
from rocrate_validator.requirements.shacl.checks import SHACLCheck
111+
from rocrate_validator.requirements.shacl.validator import SHACLValidationContext
112+
113+
target = next((p for p in profiles if p.identifier == context.settings.profile_identifier), None)
114+
if target is None:
115+
return
116+
117+
shacl_context = SHACLValidationContext.get_instance(context)
118+
# If pyshacl already ran for the target during the main loop there is
119+
# nothing to do.
120+
if shacl_context.get_validation_result(target) is not None:
121+
return
122+
123+
# Pick any SHACLCheck across the loaded profiles to drive the run; the
124+
# check identity is only used for logging inside __do_execute_check__,
125+
# the actual validation is graph-wide.
126+
runner = next(
127+
(c for p in profiles for r in p.requirements for c in r.get_checks() if isinstance(c, SHACLCheck)),
128+
None,
129+
)
130+
if runner is None:
131+
return
132+
133+
# Make sure the target's shapes (if any) are in the merged registry
134+
# and switch the current profile so violations are attributed under
135+
# the target profile in the report.
136+
shacl_context.__set_current_validation_profile__(target)
137+
shacl_context._current_validation_profile = target
138+
try:
139+
runner.__do_execute_check__(shacl_context)
140+
except Exception as e:
141+
logger.warning("Forced SHACL run for zero-shape target profile %s failed: %s", target.identifier, e)
142+
if logger.isEnabledFor(logging.DEBUG):
143+
logger.exception(e)
144+
finally:
145+
shacl_context.__unset_current_validation_profile__()
146+
147+
# do finalization logic here (empty for now)
148+
logger.debug("Completed %s requirement finalization for context %s", cls.__name__, context)
85149

86-
class SHACLRequirementLoader(RequirementLoader):
87150

151+
class SHACLRequirementLoader(RequirementLoader):
88152
def __init__(self, profile: Profile):
89153
super().__init__(profile)
90154
self._shape_registry = ShapesRegistry.get_instance(profile)

0 commit comments

Comments
 (0)