diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 30a2bd0..44ea685 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -28,30 +28,29 @@ env: VENV_PATH: .venv jobs: - # TODO: refactor this using poetry # Verifies pep8, pyflakes and circular complexity - # flake8: - # name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) - # runs-on: ubuntu-latest - # strategy: - # matrix: - # python-version: ["3.11"] - # steps: - # # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - # - uses: actions/checkout@v4 - # - name: Set up Python v${{ matrix.python-version }} - # uses: actions/setup-python@v5 - # with: - # python-version: ${{ matrix.python-version }} - # - name: Install flake8 - # run: pip install flake8 - # - name: Run checks - # run: flake8 -v . + flake8: + name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + - name: Set up Python v${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install flake8 + run: pip install flake8 + - name: Run checks + run: flake8 -v rocrate_validator tests test: name: "Tests" runs-on: ubuntu-latest - # needs: [flake8] + needs: [flake8] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py index a8dd3c6..412b98c 100644 --- a/rocrate_validator/cli/commands/errors.py +++ b/rocrate_validator/cli/commands/errors.py @@ -50,7 +50,8 @@ def handle_error(e: Exception, console: Console) -> None: if logger.isEnabledFor(logging.DEBUG): console.print_exception() console.print(textwrap.indent("This error may be due to a bug.\n" - "Please report it to the issue tracker along with the following stack trace:\n", ' ' * 9)) + "Please report it to the issue tracker " + "along with the following stack trace:\n", ' ' * 9)) console.print_exception() console.print(f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 49c2a9e..4335f0c 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -263,7 +263,8 @@ def __verbose_describe_profile__(profile): # Uncomment the following lines to show the overridden checks # if check.overridden_by: # severity_color = get_severity_color(check.overridden_by.severity) - # override = f"[overridden by: [bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ + # override = "[overridden by: " \ + # f"[bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ # f"[{severity_color}]{check.overridden_by.relative_identifier}[/{severity_color}][/bold]]" if check.override: severity_color = get_severity_color(check.override.severity) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index ca28f89..7a2892a 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -97,7 +97,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", console.print(char, end=end if choices and char in choices else "") if choices and char not in choices: if console: - console.print(f" [bold red]INVALID CHOICE[/bold red]", end=end) + console.print(" [bold red]INVALID CHOICE[/bold red]", end=end) return char @@ -275,11 +275,24 @@ def validate(ctx, logger.info("Auto-detection of the profiles to use for validation is disabled") # Prompt the user to select the profile to use for validation if the interactive mode is enabled - # and no profile is autodetected or multiple profiles are detected - if interactive and (not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles)): + # and no profile is auto-detected or multiple profiles are detected + if interactive and ( + not candidate_profiles or + len(candidate_profiles) == 0 or + len(candidate_profiles) == len(available_profiles) + ): # Define the list of choices - console.print(Padding(Rule("[bold yellow]WARNING: [/bold yellow]" - "[bold]Unable to automatically detect the profile to use for validation[/bold]\n", align="center", style="bold yellow"), (2, 2, 0, 2))) + console.print( + Padding( + Rule( + "[bold yellow]WARNING: [/bold yellow]" + "[bold]Unable to automatically detect the profile to use for validation[/bold]\n", + align="center", + style="bold yellow" + ), + (2, 2, 0, 2) + ) + ) selected_options = multiple_choice(console, available_profiles) profile_identifier = [available_profiles[int( selected_option)].identifier for selected_option in selected_options] @@ -331,7 +344,10 @@ def validate(ctx, verbose_choice = "n" if interactive and not verbose and enable_pager: verbose_choice = get_single_char(console, choices=['y', 'n'], - message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") + message=( + "[bold] > Do you want to see the validation details? " + "([magenta]y/n[/magenta]): [/bold]" + )) if verbose_choice == "y" or verbose: report_layout.show_validation_details(pager, enable_pager=enable_pager) @@ -378,7 +394,6 @@ def multiple_choice(console: Console, ] console.print("\n") selected = prompt(question, style={"questionmark": "#ff9d00 bold", - "questionmark": "#e5c07b", "question": "bold", "checkbox": "magenta", "answer": "magenta"}, @@ -508,8 +523,11 @@ def __init_layout__(self): base_info_layout = Layout( Align( f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(settings['data_path']).uri}[/bold]" - f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta] { '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}" - f"\n[bold cyan]Validation Severity:[/bold cyan] [bold {severity_color}]{settings['requirement_severity']}[/bold {severity_color}]", + "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " + f"{settings['profile_identifier']}[/bold magenta] " + f"{ '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}" + f"\n[bold cyan]Validation Severity:[/bold cyan] " + f"[bold {severity_color}]{settings['requirement_severity']}[/bold {severity_color}]", style="white", align="left"), name="Base Info", size=5) # @@ -557,12 +575,12 @@ def __init_layout__(self): # Create the main layout self.checks_stats_layout = Layout( - Panel(report_container_layout, title=f"[bold]- Validation Report -[/bold]", + Panel(report_container_layout, title="[bold]- Validation Report -[/bold]", border_style="cyan", title_align="center", padding=(1, 2))) # Create the overall result layout self.overall_result = Layout( - Padding(Rule(f"\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3) + Padding(Rule("\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3) group_layout = Layout() group_layout.add_split(self.checks_stats_layout) @@ -641,11 +659,13 @@ def set_overall_result(self, result: ValidationResult): self.result = result if result.passed(): self.overall_result.update( - Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n", + Padding(Rule("[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] " + f"[magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n", style="bold green"), (1, 1))) else: self.overall_result.update( - Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n", + Padding(Rule("[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] " + f"[magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n", style="bold red"), (1, 1))) def show_validation_details(self, pager: Pager, enable_pager: bool = True): @@ -674,7 +694,8 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") ) console.print( - f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", + f" [bold][cyan][{requirement.order_number}] " + "[u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", style="white", ) console.print(Padding(Markdown(requirement.description), (1, 7))) @@ -685,7 +706,10 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(check.level.severity) console.print( - Padding(f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold") + Padding( + f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] " + f"[magenta]{check.name}[/magenta][/bold]:", (0, 7)), + style="white bold") console.print(Padding(Markdown(check.description), (0, 27))) console.print(Padding("[u] Detected issues [/u]", (0, 8)), style="white bold") for issue in sorted(result.get_issues_by_check(check), @@ -743,7 +767,8 @@ def __compute_profile_stats__(validation_settings: dict): check_count_by_severity[severity] = 0 if severity_validation <= severity: num_checks = len( - [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) if not _.overridden]) + [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) + if not _.overridden]) check_count_by_severity[severity] += num_checks total_checks += num_checks diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 778373b..59ae62b 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -86,7 +86,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ except Exception as e: console.print( f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") - console.print("""This error may be due to a bug. + console.print("""This error may be due to a bug. Please report it to the issue tracker along with the following stack trace: """) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 9c5c872..92844e8 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -97,7 +97,7 @@ def __str__(self) -> str: return msg def __repr__(self): - return f"ProfileSpecificationNotFound()" + return "ProfileSpecificationNotFound()" class ProfileSpecificationError(ROCValidatorError): diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 6667ba4..22119a3 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -150,8 +150,10 @@ def get(name: str) -> RequirementLevel: class Profile: # store the map of profiles: profile URI -> Profile instance - __profiles_map: MultiIndexMap = MultiIndexMap("uri", - indexes=[MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True)]) + __profiles_map: MultiIndexMap = \ + MultiIndexMap("uri", indexes=[ + MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True) + ]) def __init__(self, profiles_base_path: Path, @@ -191,13 +193,17 @@ def __init__(self, self._token, self._version = self.__init_token_version__() # add the profile to the profiles map self.__profiles_map.add( - self._profile_node.toPython(), self, token=self.token, name=self.name, identifier=self.identifier) # add the profile to the profiles map + self._profile_node.toPython(), + self, token=self.token, + name=self.name, identifier=self.identifier + ) # add the profile to the profiles map else: raise ProfileSpecificationError( message=f"Profile specification file {spec_file} must contain exactly one profile") - def __get_specification_property__(self, property: str, namespace: Namespace, - pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]: + def __get_specification_property__( + self, property: str, namespace: Namespace, + pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]: assert self._profile_specification_graph is not None, "Profile specification graph not loaded" values = list(self._profile_specification_graph.objects(self._profile_node, namespace[property])) if values and as_Python_object: @@ -306,15 +312,15 @@ def __get_nested_profiles__(cls, source: str) -> list[str]: queue = [source] while len(queue) > 0: p = queue.pop() - if not p in visited: + if p not in visited: visited.append(p) profile = cls.__profiles_map.get_by_key(p) inherited_profiles = profile.is_profile_of if inherited_profiles: for p in sorted(inherited_profiles, reverse=True): - if not p in visited: + if p not in visited: queue.append(p) - if not p in result: + if p not in result: result.insert(0, p) return result @@ -1234,7 +1240,8 @@ def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings: class ValidationEvent(Event): - def __init__(self, event_type: EventType, validation_result: Optional[ValidationResult] = None, message: Optional[str] = None): + def __init__(self, event_type: EventType, + validation_result: Optional[ValidationResult] = None, message: Optional[str] = None): super().__init__(event_type, message) self._validation_result = validation_result @@ -1275,7 +1282,9 @@ def validation_result(self) -> Optional[bool]: class RequirementCheckValidationEvent(Event): - def __init__(self, event_type: EventType, requirement_check: RequirementCheck, validation_result: Optional[bool] = None, message: Optional[str] = None): + def __init__(self, event_type: EventType, + requirement_check: RequirementCheck, + validation_result: Optional[bool] = None, message: Optional[str] = None): assert event_type in (EventType.REQUIREMENT_CHECK_VALIDATION_START, EventType.REQUIREMENT_CHECK_VALIDATION_END) super().__init__(event_type, message) self._requirement_check = requirement_check @@ -1545,8 +1554,9 @@ def __load_profiles__(self) -> list[Profile]: # raised when the profile is not found if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - raise ProfileNotFound(self.profile_identifier, - message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e + raise ProfileNotFound( + self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e # Set the profiles to validate against as the target profile and its inherited profiles profiles = profile.inherited_profiles + [profile] diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py index c5122d3..e58b5fc 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py @@ -24,13 +24,14 @@ @requirement(name="Web-based Data Entity: RECOMMENDED resource availability") class WebDataEntityRecommendedChecker(PyFunctionCheck): """ - Web-based Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web-based Data Entity. + Web-based Data Entity instances SHOULD be available + at the URIs specified in the `@id` property of the Web-based Data Entity. """ @check(name="Web-based Data Entity: resource availability") def check_availability(self, context: ValidationContext) -> bool: """ - Check if the Web-based Data Entity is directly downloadable + Check if the Web-based Data Entity is directly downloadable by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs """ result = True @@ -62,7 +63,8 @@ def check_content_size(self, context: ValidationContext) -> bool: content_size = entity.get_property("contentSize") if content_size and int(content_size) != context.ro_crate.get_external_file_size(entity.id): context.result.add_check_issue( - f'The property contentSize={content_size} of the Web-based Data Entity {entity.id} does not match the actual size of ' + f'The property contentSize={content_size} of the Web-based Data Entity ' + f'{entity.id} does not match the actual size of ' f'the downloadable content, i.e., {entity.content_size} (bytes)', self, focusNode=entity.id, resultPath='contentSize', value=content_size) result = False diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py index dd5be1f..08a3685 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py @@ -33,7 +33,7 @@ def check_workflow_diagram(self, context: ValidationContext) -> bool: image = main_workflow.get_property("image") diagram_relpath = image.id if image else None if not diagram_relpath: - context.result.add_error(f"main workflow does not have an 'image' property", self) + context.result.add_error("main workflow does not have an 'image' property", self) return False if not image.is_available(): context.result.add_error(f"Workflow diagram '{image.id}' not found in crate", self) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 1fe9ceb..5ed2ed5 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -69,13 +69,15 @@ def execute_check(self, context: ValidationContext): logger.debug("SHACL Validation of profile %s requirement %s started", self.requirement.profile.identifier, self.identifier) result = self.__do_execute_check__(ctx) - ctx.current_validation_result = not self in result + ctx.current_validation_result = self not in result return ctx.current_validation_result except SHACLValidationAlreadyProcessed as e: logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile.identifier) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) # The check belongs to a profile which has already been processed # so we can skip the validation and return the specific result for the check - return not self in [i.check for i in context.result.get_issues()] + return self not in [i.check for i in context.result.get_issues()] except SHACLValidationSkip as e: logger.debug("SHACL Validation of profile %s requirement %s skipped", self.requirement.profile.identifier, self.identifier) @@ -161,12 +163,13 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ shacl_context.settings.get("target_only_validation", False): for violation in failed_requirements_checks_violations[requirementCheck.identifier]: - c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), - check=requirementCheck, - severity=violation.get_result_severity(), - resultPath=violation.resultPath.toPython() if violation.resultPath else None, - focusNode=make_uris_relative( - violation.focusNode.toPython(), shacl_context.publicID), + c = shacl_context.result.add_check_issue( + message=violation.get_result_message(shacl_context.rocrate_path), + check=requirementCheck, + severity=violation.get_result_severity(), + resultPath=violation.resultPath.toPython() if violation.resultPath else None, + focusNode=make_uris_relative( + violation.focusNode.toPython(), shacl_context.publicID), value=violation.value) # if the fail fast mode is enabled, stop the validation after the first issue if shacl_context.fail_fast: @@ -177,7 +180,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # They are issues which have not been notified yet because skipped during # the validation of their corresponding profile because SHACL checks are executed # all together and not profile by profile - if not requirementCheck.identifier in failed_requirement_checks_notified: + if requirementCheck.identifier not in failed_requirement_checks_notified: # if requirementCheck.requirement.profile != shacl_context.current_validation_profile: failed_requirement_checks_notified.append(requirementCheck.identifier) @@ -198,8 +201,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Skipped check is not a SHACLCheck: %s", requirementCheck.identifier) continue if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ - not requirementCheck in failed_requirements_checks and \ - not requirementCheck.identifier in failed_requirement_checks_notified: + requirementCheck not in failed_requirements_checks and \ + requirementCheck.identifier not in failed_requirement_checks_notified: failed_requirement_checks_notified.append(requirementCheck.identifier) shacl_context.result.add_executed_check(requirementCheck, True) shacl_context.validator.notify(RequirementCheckValidationEvent( diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 11c5414..0e0c1d8 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -34,8 +34,8 @@ class SHACLNode: # define default values - name: str = None - description: str = None + _name: str = None + _description: str = None severity: str = None def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None): @@ -54,6 +54,28 @@ def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None) # inject attributes of the shape to the object inject_attributes(self, graph, node) + @property + def name(self) -> str: + """Return the name of the shape""" + if not self._name: + self._name = self._node.split("#")[-1] if "#" in self.node else self._node.split("/")[-1] + return self._name or self._node.split("/")[-1] + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + """Return the description of the shape""" + if not self._description: + self._description = f"Check properties of the \"**{self.name}**\" entity" + return self._description + + @description.setter + def description(self, value: str): + self._description = value + @property def key(self) -> str: """Return the key of the shape""" @@ -168,8 +190,9 @@ class PropertyGroup(SHACLNodeCollection): class PropertyShape(Shape): # define default values - name: str = None - description: str = None + _name: str = None + _short_name: str = None + _description: str = None group: str = None defaultValue: str = None order: int = 0 @@ -185,6 +208,40 @@ def __init__(self, # store the parent shape self._parent = parent + @property + def name(self) -> str: + """Return the name of the shape property""" + if not self._name: + # get the object of the predicate sh:path + shacl_ns = Namespace(SHACL_NS) + path = self.graph.value(subject=self.node, predicate=shacl_ns.path) + if path: + self._short_name = path.split("#")[-1] if "#" in path else path.split("/")[-1] + if self.parent: + self._name = f"{self._short_name} of {self.parent.name}" + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + """Return the description of the shape property""" + if not self._description: + # get the object of the predicate sh:description + property_name = self.name + if self._short_name: + property_name = self._short_name + self._description = f"Check the property \"**{property_name}**\"" + if self.parent and self.parent.name not in property_name: + self._description += f" of the entity \"**{self.parent.name}**\"" + return self._description + + @description.setter + def description(self, value: str): + self._description = value + @property def node(self) -> Node: """Return the node of the shape property""" @@ -307,7 +364,7 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N property_shape, property_graph, shape) shape.add_property(p_shape) group = __process_property_group__(property_groups, p_shape) - if group and not group in shapes: + if group and group not in shapes: grouped = True shapes.append(group) if not group: diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index b1a9331..bc009dc 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -16,14 +16,14 @@ import hashlib from pathlib import Path -from typing import Union +from typing import Optional, Union from rdflib import RDF, BNode, Graph, Namespace from rdflib.term import Node +import rocrate_validator.log as logging from rocrate_validator.constants import RDF_SYNTAX_NS, SHACL_NS from rocrate_validator.errors import BadSyntaxError -import rocrate_validator.log as logging from rocrate_validator.models import Severity # set up logging @@ -70,10 +70,10 @@ def make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: return text.replace(str(ro_crate_path), './') -def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: +def inject_attributes(obj: object, node_graph: Graph, node: Node, exclude: Optional[list] = None) -> object: # inject attributes of the shape property # logger.debug("Injecting attributes of node %s", node) - skip_properties = ["node"] + skip_properties = ["node"] if exclude is None else exclude + ["node"] triples = node_graph.triples((node, None, None)) for node, p, o in triples: predicate_as_string = p.toPython() @@ -229,18 +229,29 @@ def load_from_graph(cls, graph: Graph) -> ShapesList: return load_shapes_from_graph(graph) -def __extract_related_triples__(graph, subject_node): +def __extract_related_triples__(graph, subject_node, processed_nodes=None): """ Recursively extract all triples related to a given shape. """ + related_triples = [] + + processed_nodes = processed_nodes if processed_nodes is not None else set() + + # Skip the current node if it has already been processed + if subject_node in processed_nodes: + return related_triples + + # Add the current node to the processed nodes + processed_nodes.add(subject_node) + # Directly related triples related_triples.extend((_, p, o) for (_, p, o) in graph.triples((subject_node, None, None))) # Recursively find triples related to nested shapes for _, _, object_node in related_triples: if isinstance(object_node, Node): - related_triples.extend(__extract_related_triples__(graph, object_node)) + related_triples.extend(__extract_related_triples__(graph, object_node, processed_nodes)) return related_triples diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 97c11fc..70213ba 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -113,7 +113,7 @@ def __init__(self, context: ValidationContext): self._ontology_graph: Graph = Graph() def __set_current_validation_profile__(self, profile: Profile) -> bool: - if not profile.identifier in self._processed_profiles: + if profile.identifier not in self._processed_profiles: # augment the ontology graph with the profile ontology ontology_graph = self.__load_ontology_graph__(profile.path) if ontology_graph: @@ -195,7 +195,8 @@ def __get_ontology_path__(self, profile_path: Path, ontology_filename: str = DEF self._ontology_path = Path(supported_path) return self._ontology_path - def __load_ontology_graph__(self, profile_path: Path, ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: + def __load_ontology_graph__(self, profile_path: Path, + ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: # load the graph of ontologies ontology_graph = None ontology_path = self.__get_ontology_path__(profile_path, ontology_filename) @@ -211,7 +212,7 @@ def __load_ontology_graph__(self, profile_path: Path, ontology_filename: Optiona def ontology_graph(self) -> Graph: return self._ontology_graph - @ classmethod + @classmethod def get_instance(cls, context: ValidationContext) -> SHACLValidationContext: instance = getattr(context, "_shacl_validation_context", None) if not instance: @@ -283,7 +284,8 @@ def sourceConstraintComponent(self): if not self._source_constraint_component: self._source_constraint_component = self.graph.value( self._violation_node, URIRef(f"{SHACL_NS}sourceConstraintComponent")) - assert self._source_constraint_component is not None, f"Unable to get source constraint component from violation node {self._violation_node}" + assert self._source_constraint_component is not None, \ + f"Unable to get source constraint component from violation node {self._violation_node}" return self._source_constraint_component def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: @@ -297,7 +299,8 @@ def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: def sourceShape(self) -> Union[URIRef, BNode]: if not self._source_shape_node: self._source_shape_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}sourceShape")) - assert self._source_shape_node is not None, f"Unable to get source shape node from violation node {self._violation_node}" + assert self._source_shape_node is not None, \ + f"Unable to get source shape node from violation node {self._violation_node}" return self._source_shape_node diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 00ed652..3b94a86 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -161,7 +161,8 @@ def __extract_and_validate_rocrate__(rocrate_path: Path): def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL, - allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> list[Profile]: + allow_requirement_check_override: bool = + ValidationSettings.allow_requirement_check_override) -> list[Profile]: """ Load the profiles from the given path """ @@ -175,7 +176,8 @@ def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, publicID: str = None, - allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> Profile: + allow_requirement_check_override: bool = + ValidationSettings.allow_requirement_check_override) -> Profile: """ Load the profiles from the given path """ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6866446 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[flake8] +max-line-length = 120 +exclude = + .git + .github + .vscode + .venv + __pycache__ + build + dist + rocrate_validator.egg-info diff --git a/tests/integration/profiles/process-run-crate/test_procrc_action.py b/tests/integration/profiles/process-run-crate/test_procrc_action.py index 0c6cbf2..bc32c4e 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_action.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_action.py @@ -261,7 +261,8 @@ def test_procrc_action_bad_actionstatus(): Severity.RECOMMENDED, False, ["Process Run Crate Action SHOULD"], - ["If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus"], + ["If the Action has an actionStatus, it should be " + "http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus"], profile_identifier="process-run-crate" ) @@ -291,7 +292,8 @@ def test_procrc_action_obj_res_bad_type(): Severity.RECOMMENDED, False, ["Process Run Crate Action object and result types"], - ["object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue"], + ["object and result SHOULD point to entities of type " + "MediaObject, Dataset, Collection, CreativeWork or PropertyValue"], profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py index 7af7ba9..e816573 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py @@ -31,7 +31,9 @@ def test_procrc_containerimage_no_additionaltype(): Severity.RECOMMENDED, False, ["Process Run Crate ContainerImage SHOULD"], - ["The ContainerImage SHOULD have an additionalType pointing to or "], + ["The ContainerImage SHOULD have an additionalType pointing " + "to or " + ""], profile_identifier="process-run-crate" ) @@ -46,7 +48,9 @@ def test_procrc_containerimage_bad_additionaltype(): Severity.RECOMMENDED, False, ["Process Run Crate ContainerImage SHOULD"], - ["The ContainerImage SHOULD have an additionalType pointing to or "], + ["The ContainerImage SHOULD have an additionalType pointing " + "to or " + ""], profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py index 7ac3fc0..255523b 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py @@ -32,7 +32,8 @@ def test_procrc_no_conformsto(): Severity.REQUIRED, False, ["Root Data Entity Metadata"], - ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI " + "that is consistent with the versioned permalink of the profile"], profile_identifier="process-run-crate" ) @@ -47,7 +48,8 @@ def test_procrc_conformsto_bad_type(): Severity.REQUIRED, False, ["Root Data Entity Metadata"], - ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI " + "that is consistent with the versioned permalink of the profile"], profile_identifier="process-run-crate" ) @@ -62,6 +64,7 @@ def test_procrc_conformsto_bad_profile(): Severity.REQUIRED, False, ["Root Data Entity Metadata"], - ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI " + "that is consistent with the versioned permalink of the profile"], profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/ro-crate/test_root_data_entity.py b/tests/integration/profiles/ro-crate/test_root_data_entity.py index 792aa5f..95a2c1d 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -66,7 +66,8 @@ def test_invalid_root_date(): models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], - ["The Root Data Entity SHOULD have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date and the precision of at least the day level"] + ["The Root Data Entity SHOULD have a `datePublished` property (as specified by schema.org) " + "with a valid ISO 8601 date and the precision of at least the day level"] ) diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index edc7dfb..e44586b 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -14,8 +14,6 @@ import logging -import pytest - from rocrate_validator.models import Severity from tests.ro_crates import ValidROC from tests.shared import do_entity_test diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py index f7b1bd6..4c14a56 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -32,6 +32,7 @@ def test_wroc_descriptor_bad_conforms_to(): Severity.RECOMMENDED, False, ["WROC Metadata File Descriptor properties"], - ["The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 and https://w3id.org/workflowhub/workflow-ro-crate/1.0"], + ["The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 " + "and https://w3id.org/workflowhub/workflow-ro-crate/1.0"], profile_identifier="workflow-ro-crate" ) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index d0f1c21..f80c39a 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -172,6 +172,7 @@ def test_versioned_profiles_loading(fake_versioned_profiles_path): def test_conflicting_versioned_profiles_loading(fake_conflicting_versioned_profiles_path): """Test the loaded profiles from the validator context.""" with pytest.raises(ProfileSpecificationError) as excinfo: + logger.debug("result: %r", excinfo) # Load the profiles Profile.load_profiles(profiles_path=fake_conflicting_versioned_profiles_path) # Check that the conflicting versions are found @@ -205,7 +206,8 @@ def __perform_test__(profile_identifier: str, expected_inherited_profiles: list[ # The number of profiles should be 1 profiles_names = [_.token for _ in profile.inherited_profiles] - assert profiles_names == expected_inherited_profiles, f"The number of profiles should be {expected_inherited_profiles}" + assert profiles_names == expected_inherited_profiles, \ + f"The number of profiles should be {expected_inherited_profiles}" # Test the inheritance mode with 1 profile __perform_test__("a", []) diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index d48924f..7350b25 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -17,7 +17,14 @@ from rocrate_validator import log as logging from rocrate_validator.errors import ROCrateInvalidURIError -from rocrate_validator.rocrate import ROCrate, ROCrateEntity, ROCrateLocalFolder, ROCrateLocalZip, ROCrateMetadata, ROCrateRemoteZip +from rocrate_validator.rocrate import ( + ROCrate, + ROCrateEntity, + ROCrateLocalFolder, + ROCrateLocalZip, + ROCrateMetadata, + ROCrateRemoteZip, +) from tests.ro_crates import ValidROC # set up logging @@ -28,7 +35,7 @@ ################################ -###### ROCrateLocalFolder ###### +# ROCrateLocalFolder ################################ @@ -83,7 +90,8 @@ def test_valid_local_rocrate(): assert isinstance(root_data_entity, ROCrateEntity), "Entity should be ROCrateEntity" assert root_data_entity.id == "./", "Id should be ./" assert root_data_entity.type == "Dataset", "Type should be Dataset" - assert root_data_entity.name == "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", "Name should be wrroc-paper" + assert root_data_entity.name == "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", \ + "Name should be wrroc-paper" # check metadata consistency assert root_data_entity.metadata == metadata, "Metadata should be the same" @@ -94,7 +102,7 @@ def test_valid_local_rocrate(): ################################ -###### ROCrateLocalZip ######### +# ROCrateLocalZip ################################ def test_valid_zip_rocrate(): roc = ROCrateLocalZip(ValidROC().sort_and_change_archive) @@ -158,7 +166,7 @@ def test_valid_zip_rocrate(): ################################ -###### ROCrateRemote ########### +# ROCrateRemoteZip ################################