Skip to content

Commit 244f398

Browse files
authored
Merge branch 'crs4:develop' into fix-cyclic-datasets
2 parents d9a90e7 + 2251168 commit 244f398

11 files changed

Lines changed: 1567 additions & 347 deletions

File tree

CHANGELOG.md

Lines changed: 753 additions & 0 deletions
Large diffs are not rendered by default.

poetry.lock

Lines changed: 645 additions & 317 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "roc-validator"
3-
version = "0.8.0"
3+
version = "0.8.1"
44
description = "A Python package to validate RO-Crates"
55
authors = [
66
"Marco Enrico Piras <kikkomep@crs4.it>",
@@ -60,7 +60,7 @@ include = [
6060
[tool.poetry.dependencies]
6161
python = ">=3.10,<4.0"
6262
rdflib = ">=7.1,<8.0"
63-
pyshacl = ">=0.26,<0.31"
63+
pyshacl = ">=0.26"
6464
click = ">=8.2,<9.0"
6565
rich = ">=13.9,<14.0"
6666
toml = ">=0.10.2,<1.0"
@@ -106,6 +106,14 @@ skip_dirs = [".git", ".github", ".vscode"]
106106

107107
[tool.pytest.ini_options]
108108
testpaths = ["tests"]
109+
filterwarnings = [
110+
# Ignore deprecation warnings from rdflib's JSON-LD parser,
111+
# used internally by pyshacl. These warnings are unrelated to
112+
# our code and currently noisy.
113+
# See: https://github.com/RDFLib/rdflib/issues/3064
114+
# Fix in progress: https://github.com/RDFLib/rdflib/issues/3302
115+
"ignore::DeprecationWarning:rdflib.plugins.parsers.jsonld",
116+
]
109117

110118
[tool.typos.files]
111-
extend-exclude = ["tests/data","docs/diagrams","*.json","*.html","*__init__.py"]
119+
extend-exclude = ["tests/data", "docs/diagrams", "*.json", "*.html", "*__init__.py"]

pytest.ini

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,10 @@
1717
; log_cli=true
1818
; log_level=DEBUG
1919
addopts = -n auto
20-
; filterwarnings =
20+
filterwarnings =
21+
# Ignore deprecation warnings from rdflib's JSON-LD parser,
22+
# used internally by pyshacl. These warnings are unrelated to
23+
# our code and currently noisy.
24+
# See: https://github.com/RDFLib/rdflib/issues/3064
25+
# Fix in progress: https://github.com/RDFLib/rdflib/issues/3302
26+
ignore::DeprecationWarning:rdflib.plugins.parsers.jsonld

rocrate_validator/cli/commands/validate.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import os
1818
import sys
19+
from contextlib import nullcontext
1920
from pathlib import Path
2021
from typing import Optional
2122

@@ -276,7 +277,7 @@ def validate(ctx,
276277
"profile_identifier": profile_identifier,
277278
"requirement_severity": requirement_severity,
278279
"requirement_severity_only": requirement_severity_only,
279-
"enable_profile_inheritance": not disable_profile_inheritance,
280+
"disable_inherited_profiles_issue_reporting": disable_profile_inheritance,
280281
"rocrate_uri": rocrate_uri,
281282
"rocrate_relative_root_path": relative_root_path,
282283
"abort_on_first": fail_fast,
@@ -472,7 +473,7 @@ def validate(ctx,
472473
console.print(f"\n{' '*2}📋 [bold]The validation report in JSON format: [/bold]\n")
473474

474475
# Generate the JSON output and write it to the specified output file or to stdout
475-
with open(output_file, "w", encoding="utf-8") if output_file else sys.stdout as f:
476+
with open(output_file, "w", encoding="utf-8") if output_file else nullcontext(sys.stdout) as f:
476477
out = Console(width=output_line_width, file=f)
477478
out.register_formatter(JSONOutputFormatter())
478479
out.print(results)

rocrate_validator/cli/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030

3131
@click.group(invoke_without_command=True)
32-
@click.rich_config(help_config=click.RichHelpConfiguration(use_rich_markup=True))
32+
@click.rich_config(help_config=click.RichHelpConfiguration(text_markup="rich"))
3333
@click.option(
3434
'--debug',
3535
is_flag=True,

rocrate_validator/models.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,26 +1007,43 @@ def _do_validate_(self, context: ValidationContext) -> bool:
10071007

10081008
logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name)
10091009
all_passed = True
1010-
for check in [_ for _ in self._checks
1011-
if not context.settings.skip_checks
1012-
or _.identifier not in context.settings.skip_checks]:
1013-
1010+
checks_to_perform = [
1011+
_ for _ in self._checks
1012+
if not context.settings.skip_checks
1013+
or _.identifier not in context.settings.skip_checks
1014+
]
1015+
for check in checks_to_perform:
10141016
try:
10151017
if check.overridden and not check.requirement.profile.identifier == context.profile_identifier:
10161018
logger.debug("Skipping check '%s' because overridden by '%r'",
10171019
check.identifier, [_.identifier for _ in check.overridden_by])
10181020
continue
1019-
context.validator.notify(RequirementCheckValidationEvent(
1020-
EventType.REQUIREMENT_CHECK_VALIDATION_START, check))
1021+
# Determine whether to skip event notification for inherited profiles
1022+
skip_event_notify = False
1023+
if check.requirement.profile.identifier != context.profile_identifier and \
1024+
context.settings.disable_inherited_profiles_issue_reporting:
1025+
logger.debug("Inherited profiles reporting disabled. "
1026+
"Skipping requirement %s as it belongs to an inherited profile %s",
1027+
check.requirement.identifier, check.requirement.profile.identifier)
1028+
skip_event_notify = True
1029+
# Notify the start of the check execution if not skip_event_notify is set to True
1030+
if not skip_event_notify:
1031+
context.validator.notify(RequirementCheckValidationEvent(
1032+
EventType.REQUIREMENT_CHECK_VALIDATION_START, check))
1033+
# Execute the check
10211034
check_result = check.execute_check(context)
10221035
logger.debug("Result of check %s: %s", check.identifier, check_result)
10231036
context.result._add_executed_check(check, check_result)
1024-
context.validator.notify(RequirementCheckValidationEvent(
1025-
EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=check_result))
1037+
# Notify the end of the check execution if not skip_event_notify is set to True
1038+
if not skip_event_notify:
1039+
context.validator.notify(RequirementCheckValidationEvent(
1040+
EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=check_result))
10261041
logger.debug("Ran check '%s'. Got result %s", check.identifier, check_result)
1042+
# Ensure the check result is a boolean
10271043
if not isinstance(check_result, bool):
10281044
logger.warning("Ignoring the check %s as it returned the value %r instead of a boolean", check.name)
10291045
raise RuntimeError(f"Ignoring invalid result from check {check.name}")
1046+
# Aggregate the check result
10301047
all_passed = all_passed and check_result
10311048
if not all_passed and context.fail_fast:
10321049
break
@@ -1040,7 +1057,8 @@ def _do_validate_(self, context: ValidationContext) -> bool:
10401057
logger.warning("Consider reporting this as a bug.")
10411058
if logger.isEnabledFor(logging.DEBUG):
10421059
logger.exception(e)
1043-
1060+
skipped_checks = set(self._checks) - set(checks_to_perform)
1061+
context.result.skipped_checks.update(skipped_checks)
10441062
logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", self.name, all_passed)
10451063
return all_passed
10461064

@@ -1625,7 +1643,7 @@ def __initialise__(cls, validation_settings: ValidationSettings):
16251643
profiles = [profile]
16261644

16271645
# add inherited profiles if enabled
1628-
if validation_settings.enable_profile_inheritance:
1646+
if not validation_settings.disable_inherited_profiles_issue_reporting:
16291647
profiles.extend(profile.inherited_profiles)
16301648
logger.debug("Inherited profiles: %r", profile.inherited_profiles)
16311649

@@ -1656,9 +1674,20 @@ def __initialise__(cls, validation_settings: ValidationSettings):
16561674
if severity < severity_validation:
16571675
continue
16581676
# count the checks
1659-
requirement_checks = [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name))
1660-
if not _.overridden or
1661-
_.requirement.profile.identifier == target_profile_identifier]
1677+
requirement_checks = [
1678+
_
1679+
for _ in requirement.get_checks_by_level(
1680+
LevelCollection.get(severity.name)
1681+
)
1682+
if (
1683+
not validation_settings.skip_checks
1684+
or _.identifier not in validation_settings.skip_checks
1685+
)
1686+
and (
1687+
not _.overridden
1688+
or _.requirement.profile.identifier == target_profile_identifier
1689+
)
1690+
]
16621691
num_checks = len(requirement_checks)
16631692
requirement_checks_count += num_checks
16641693
if num_checks > 0:
@@ -2325,12 +2354,20 @@ class ValidationSettings:
23252354
#: The profile identifier to validate against
23262355
profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER
23272356
#: Flag to enable profile inheritance
2357+
# Use the `enable_profile_inheritance` flag with caution: disable inheritance only if the
2358+
# target validation profile is fully self-contained and does not rely on definitions
2359+
# from inherited profiles (e.g., entities defined upstream). For modularization
2360+
# purposes, some base entities and properties are defined in the base RO-Crate
2361+
# profile and are intentionally not redefined in specialized profiles; they are
2362+
# required for validations targeting those specializations and therefore cannot be skipped.
2363+
# Nevertheless, the validator can still suppress issue reporting for checks defined
2364+
# in inherited profiles by setting disable_inherited_profiles_issue_reporting to `True`.
23282365
enable_profile_inheritance: bool = True
23292366
# Validation settings
23302367
#: Flag to abort on first error
23312368
abort_on_first: Optional[bool] = False
2332-
#: Flag to disable inherited profiles reporting
2333-
disable_inherited_profiles_reporting: bool = False
2369+
#: Flag to disable reporting of issues related to inherited profiles
2370+
disable_inherited_profiles_issue_reporting: bool = False
23342371
#: Flag to disable remote crate download
23352372
disable_remote_crate_download: bool = True
23362373
# Requirement settings

rocrate_validator/requirements/python/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ def __init__(self,
5555
self._check_function = check_function
5656

5757
def execute_check(self, context: ValidationContext) -> bool:
58+
if self.requirement.profile.identifier != context.profile_identifier and \
59+
context.settings.disable_inherited_profiles_issue_reporting:
60+
logger.debug("Skipping requirement %s as it belongs to an inherited profile %s",
61+
self.requirement.identifier, self.requirement.profile.identifier)
62+
return True
5863
return self._check_function(self, context)
5964

6065

rocrate_validator/requirements/shacl/checks.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,19 +200,21 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext):
200200
assert shape is not None, "Unable to map the violation to a shape"
201201
requirementCheck = SHACLCheck.get_instance(shape)
202202
assert requirementCheck is not None, "The requirement check cannot be None"
203-
failed_requirements_checks.add(requirementCheck)
204-
violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None)
205-
if violations is None:
206-
failed_requirements_checks_violations[requirementCheck.identifier] = violations = []
207-
violations.append(violation)
203+
if (not shacl_context.settings.skip_checks or
204+
requirementCheck.identifier not in shacl_context.settings.skip_checks):
205+
failed_requirements_checks.add(requirementCheck)
206+
violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None)
207+
if violations is None:
208+
failed_requirements_checks_violations[requirementCheck.identifier] = (violations := [])
209+
violations.append(violation)
208210
# sort the failed checks by identifier and severity
209211
# to ensure a consistent order of the issues
210212
# and to make the fail fast mode deterministic
211213
for requirementCheck in sorted(failed_requirements_checks, key=lambda x: (x.identifier, x.severity)):
212214
# if the check is not in the current profile
213215
# and the disable_inherited_profiles_reporting is enabled, skip it
214216
if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \
215-
shacl_context.settings.disable_inherited_profiles_reporting:
217+
shacl_context.settings.disable_inherited_profiles_issue_reporting:
216218
continue
217219
for violation in failed_requirements_checks_violations[requirementCheck.identifier]:
218220
violating_entity = make_uris_relative(violation.focusNode.toPython(), shacl_context.publicID)
@@ -273,6 +275,9 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext):
273275
if requirementCheck.identifier not in failed_requirement_checks_notified:
274276
failed_requirement_checks_notified.append(requirementCheck.identifier)
275277
shacl_context.result._add_executed_check(requirementCheck, True)
278+
if requirementCheck.requirement.profile != shacl_context.target_profile and \
279+
shacl_context.settings.disable_inherited_profiles_issue_reporting:
280+
continue
276281
shacl_context.validator.notify(RequirementCheckValidationEvent(
277282
EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=True))
278283
logger.debug("Added skipped check to the context: %s", requirementCheck.identifier)

tests/unit/test_services.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from rocrate_validator.models import ValidationSettings
2222
from rocrate_validator.rocrate import ROCrateMetadata
2323
from rocrate_validator.services import detect_profiles, get_profiles, validate
24-
from tests.ro_crates import InvalidMultiProfileROC, ValidROC
24+
from tests.ro_crates import InvalidMultiProfileROC, ValidROC, InvalidFileDescriptorEntity
2525

2626
# set up logging
2727
logger = logging.getLogger(__name__)
@@ -106,6 +106,64 @@ def test_valid_local_workflow_testing_ro_crate():
106106
"Expected the 'workflow-testing-ro-crate-0.1' profile"
107107

108108

109+
def test_disable_inherited_profiles_issue_reporting():
110+
# Set the rocrate_uri to the workflow testing RO-Crate
111+
crate_path = ValidROC().workflow_testing_ro_crate
112+
logger.debug("Validating a local RO-Crate: %s", crate_path)
113+
114+
# First, validate with inherited profiles issue reporting enabled
115+
settings = ValidationSettings(
116+
rocrate_uri=crate_path,
117+
disable_inherited_profiles_issue_reporting=False
118+
)
119+
result = validate(settings)
120+
total_issues_with_inheritance = len(result.get_issues())
121+
logger.debug("Total issues with inherited profiles issue reporting enabled: %d", total_issues_with_inheritance)
122+
123+
# Now, validate with inherited profiles issue reporting disabled
124+
settings.disable_inherited_profiles_issue_reporting = True
125+
result = validate(settings)
126+
total_issues_without_inheritance = len(result.get_issues())
127+
logger.debug("Total issues with inherited profiles issue reporting disabled: %d", total_issues_without_inheritance)
128+
129+
# Check that disabling inherited profiles issue reporting reduces the number of reported issues
130+
assert total_issues_without_inheritance <= total_issues_with_inheritance, \
131+
"Disabling inherited profiles issue reporting should not increase the number of reported issues"
132+
133+
# Check that all reported issues are from the main profile
134+
main_profile_identifier = "workflow-testing-ro-crate-0.1"
135+
for issue in result.get_issues():
136+
assert issue.check.profile.identifier == main_profile_identifier, \
137+
"All reported issues should belong to the main profile when inherited profiles issue reporting is disabled"
138+
139+
140+
def test_skip_pycheck_on_workflow_ro_crate():
141+
# Set the rocrate_uri to the workflow testing RO-Crate
142+
crate_path = InvalidFileDescriptorEntity().invalid_conforms_to
143+
logger.debug("Validating a local RO-Crate: %s", crate_path)
144+
settings = ValidationSettings(rocrate_uri=crate_path)
145+
result = validate(settings)
146+
assert not result.passed(), \
147+
"The RO-Crate is expected to be invalid because of an incorrect conformsTo field and missing resources"
148+
assert len(result.failed_checks) == 2, "No failed checks expected when skipping the problematic check"
149+
assert any(check.identifier == "ro-crate-1.1_5.3" for check in result.failed_checks), \
150+
"Expected the check 'ro-crate-1.1_5.3' to fail"
151+
assert any(check.identifier == "ro-crate-1.1_12.1" for check in result.failed_checks), \
152+
"Expected the check 'ro-crate-1.1_12.1' to fail"
153+
154+
# Perform a new validation skipping specific checks
155+
settings.skip_checks = ["ro-crate-1.1_5.3", "ro-crate-1.1_12.1"]
156+
result = validate(settings)
157+
assert result.passed(), \
158+
"The RO-Crate should be valid when skipping the checks related to the invalid file descriptor entity"
159+
160+
# Ensure that the skipped checks are indeed skipped
161+
skipped_check_ids = {check.identifier for check in result.skipped_checks}
162+
# logger.error("Skipped checks: %s", result.skipped_checks)
163+
assert "ro-crate-1.1_5.3" in skipped_check_ids, "Expected check 'ro-crate-1.1_5.3' to be skipped"
164+
assert "ro-crate-1.1_12.1" in skipped_check_ids, "Expected check 'ro-crate-1.1_12.1' to be skipped"
165+
166+
109167
def test_valid_local_multi_profile_crate():
110168
# Set the rocrate_uri to the multi-profile RO-Crate
111169
crate_path = InvalidMultiProfileROC().invalid_multi_profile_crate

0 commit comments

Comments
 (0)