Skip to content

Commit 6cde639

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into develop-testmerge
2 parents 9942e52 + 7277042 commit 6cde639

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+6154
-348
lines changed

CHANGELOG.md

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

CITATION.cff

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ authors:
1919
- family-names: Bauer
2020
given-names: Daniel
2121
orcid: https://orcid.org/0000-0001-9447-460X
22+
- family-names: "Wetzels"
23+
given-names: "Florian"
24+
orcid: https://orcid.org/0000-0002-5526-7138
25+
- family-names: "Weil"
26+
given-names: "Heinrich Lukas"
27+
orcid: https://orcid.org/0000-0003-1945-6342
2228
repository-code: "https://github.com/crs4/rocrate-validator"
2329
url: "https://github.com/crs4/rocrate-validator"
2430
keywords:

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,9 +106,17 @@ 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"]
112120

113121
[tool.typos.default.extend-words]
114122
TRE = "TRE"

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
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright (c) 2026 DataPLANT
2+
# Copyright (c) 2026 The University of Manchester
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
@prefix ro: <./> .
17+
@prefix ro-crate: <https://github.com/crs4/rocrate-validator/profiles/ro-crate/> .
18+
@prefix isa-ro-crate: <https://github.com/crs4/rocrate-validator/profiles/isa-ro-crate/> .
19+
@prefix bioschemas: <https://bioschemas.org/> .
20+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
21+
@prefix schema: <http://schema.org/> .
22+
@prefix sh: <http://www.w3.org/ns/shacl#> .
23+
@prefix validator: <https://github.com/crs4/rocrate-validator/> .
24+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
25+
26+
isa-ro-crate:RootDataEntityMustBeInvestigation a sh:NodeShape ;
27+
sh:name "Root Data Entity must be Investigation" ;
28+
sh:description "The root data entity must follow the investigation profile" ;
29+
sh:targetClass ro-crate:RootDataEntity ;
30+
sh:property [
31+
a sh:PropertyShape ;
32+
sh:path schema:additionalType ;
33+
sh:minCount 1 ;
34+
sh:hasValue "Investigation";
35+
sh:description "Check if the root data entity is specified as an investigation through additionalType" ;
36+
sh:message "The root data entity must have additionalType of `Investigation`" ;
37+
sh:severity sh:Violation ;
38+
] ;
39+
sh:property [
40+
a sh:PropertyShape ;
41+
sh:path schema:identifier ;
42+
sh:minCount 1 ;
43+
sh:datatype xsd:string ;
44+
sh:not [
45+
sh:hasValue ""
46+
] ;
47+
sh:description "Check if the root data entity (investigation) has an identifier" ;
48+
sh:message "The root data entity must have a non-empty identifier" ;
49+
sh:severity sh:Violation ;
50+
] ;
51+
# sh:property [
52+
# a sh:PropertyShape ;
53+
# sh:path schema:name ;
54+
# sh:minCount 1 ;
55+
# sh:not [
56+
# sh:hasValue ""
57+
# ] ;
58+
# sh:description "Check if the root data entity (investigation) has a name" ;
59+
# sh:message "The root data entity must have a non-empty name" ;
60+
# sh:severity sh:Violation ;
61+
# ] ;
62+
# sh:property [
63+
# a sh:PropertyShape ;
64+
# sh:path schema:description ;
65+
# sh:minCount 1 ;
66+
# sh:not [
67+
# sh:hasValue ""
68+
# ] ;
69+
# sh:description "Check if the root data entity (investigation) has a description" ;
70+
# sh:message "The root data entity must have a non-empty description" ;
71+
# sh:severity sh:Violation ;
72+
# ] ;
73+
# sh:property [
74+
# a sh:PropertyShape ;
75+
# sh:path schema:license ;
76+
# sh:minCount 1 ;
77+
# sh:description "Check if the root data entity (investigation) specifies a license" ;
78+
# sh:message "The root data entity must specify a license" ;
79+
# sh:severity sh:Violation ;
80+
# ] ;
81+
# sh:property [
82+
# a sh:PropertyShape ;
83+
# sh:path schema:datePublished ;
84+
# sh:minCount 1 ;
85+
# sh:nodeKind sh:Literal ;
86+
# sh:pattern "^([\\+-]?\\d{4})((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))|W([0-4]\\d|5[0-2])(-?[1-7])|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)?[0-5]\\d)?|24:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)$" ;
87+
# sh:description "Check if the root data entity (investigation) has a datePublished" ;
88+
# sh:message "The root data entity must have a datePublished" ;
89+
# sh:severity sh:Violation ;
90+
# ] ;
91+
.
92+
93+
isa-ro-crate:InvestigationShouldHaveCreator a sh:NodeShape ;
94+
sh:name "Investigation SHOULD have creator" ;
95+
sh:description "An Investigation SHOULD have a creator" ;
96+
sh:targetClass ro-crate:RootDataEntity ;
97+
sh:property [
98+
a sh:PropertyShape ;
99+
sh:path schema:creator ;
100+
sh:minCount 1 ;
101+
sh:description "Check that investigation does have at least one creator" ;
102+
sh:message "Investigation entity SHOULD have a creator" ;
103+
sh:severity sh:Warning ;
104+
] ;
105+
sh:property [
106+
a sh:PropertyShape ;
107+
sh:path schema:creator ;
108+
sh:class schema:Person ;
109+
sh:description "Check that if investigation does have at least one creator, it MUST be of type Person" ;
110+
sh:message "Investigation creator MUST be of type Person" ;
111+
sh:severity sh:Violation ;
112+
]
113+
.
114+
115+
isa-ro-crate:InvestigationShouldHaveDateCreated a sh:NodeShape ;
116+
sh:name "Investigation SHOULD have dateCreated" ;
117+
sh:description "An Investigation SHOULD have a dateCreated" ;
118+
sh:targetClass ro-crate:RootDataEntity ;
119+
sh:property [
120+
a sh:PropertyShape ;
121+
sh:path schema:dateCreated ;
122+
sh:minCount 1 ;
123+
sh:description "Check that investigation does have at least one dateCreated" ;
124+
sh:message "Investigation entity SHOULD have a dateCreated" ;
125+
sh:severity sh:Warning ;
126+
] ;
127+
sh:property [
128+
a sh:PropertyShape ;
129+
sh:path schema:dateCreated ;
130+
sh:nodeKind sh:Literal ;
131+
sh:pattern "^([\\+-]?\\d{4})((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))|W([0-4]\\d|5[0-2])(-?[1-7])|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)?[0-5]\\d)?|24:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)$" ;
132+
sh:description "Check that if investigation does have at least one dateCreated, it MUST be a valid ISO 8601 date." ;
133+
sh:message "Investigation dateCreated MUST be a valid ISO 8601 date" ;
134+
sh:severity sh:Violation ;
135+
]
136+
.

0 commit comments

Comments
 (0)