|
19 | 19 |
|
20 | 20 | from rocrate_validator.utils import log as logging |
21 | 21 | 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 | +) |
24 | 30 | from rocrate_validator.requirements.shacl.checks import SHACLCheck |
25 | 31 | from rocrate_validator.requirements.shacl.models import Shape, ShapesRegistry |
26 | 32 |
|
@@ -82,9 +88,67 @@ def hidden(self) -> bool: |
82 | 88 | return True |
83 | 89 | return False |
84 | 90 |
|
| 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) |
85 | 149 |
|
86 | | -class SHACLRequirementLoader(RequirementLoader): |
87 | 150 |
|
| 151 | +class SHACLRequirementLoader(RequirementLoader): |
88 | 152 | def __init__(self, profile: Profile): |
89 | 153 | super().__init__(profile) |
90 | 154 | self._shape_registry = ShapesRegistry.get_instance(profile) |
|
0 commit comments