Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Enhanced Targeting Features. #249

Merged
merged 14 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Python PEP 440 Versioning](https://www.python.org/dev/peps/pep-0440/).

## [Unreleased]

### Added
- Focus Node mode!
- You can now pass in a list of focus nodes to the validator, and it will only validate those focus nodes.
- Note, you still need to pass in a SHACL Shapes Graph, and the shapes still need to target the focus nodes.
- This feature will filter the Shapes' targeted focus nodes to include only those that are in the list of specified focus nodes.

### Changed
- Don't make a clone of the DataGraph if the input data graph is ephemeral.
- An ephemeral graph is one that is loaded from a string or file location by PySHACL
- This includes all files opened by the PySHACL CLI validator tool
- We don't need to make a copy because PySHACL parsed the Graph into memory itself already, so we are not concerned about not polluting the user's graph.
- Refactorings
- shacl_path_to_sparql_path code to a reusable importable function
- move sht_validate and dash_validate routes to `validator_conformance.py` module.
- Removes some complexity from the main `validate` function.
- Typing
- A whole swathe of python typing fixes and new type annotations. Thanks @ajnelson-nist
### Fixed
- Fix logic determining if a datagraph is ephemeral.


## [0.26.0] - 2024-04-11
Expand Down
10 changes: 10 additions & 0 deletions pyshacl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ def str_is_true(s_var: str):
parser.add_argument(
'-d', '--debug', dest='debug', action='store_true', default=False, help='Output additional runtime messages.'
)
parser.add_argument(
'--focus',
dest='focus',
action='store',
help='The IRI of a focus node from the DataGraph, the shapes will validate only that node.',
nargs="?",
default=None,
)
parser.add_argument(
'-f',
'--format',
Expand Down Expand Up @@ -259,6 +267,8 @@ def main(prog: Union[str, None] = None) -> None:
validator_kwargs['advanced'] = True
if args.js:
validator_kwargs['js'] = True
if args.focus:
validator_kwargs['focus'] = args.focus
if args.iterate_rules:
if not args.advanced:
sys.stderr.write("Iterate-Rules option only works when you enable Advanced Mode.\n")
Expand Down
5 changes: 3 additions & 2 deletions pyshacl/pytypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
#

from dataclasses import dataclass
from typing import Optional, Union
from typing import List, Optional, Union

from rdflib import ConjunctiveGraph, Dataset, Graph, Literal
from rdflib.term import IdentifiedNode
from rdflib.term import IdentifiedNode, URIRef

ConjunctiveLike = Union[ConjunctiveGraph, Dataset]
GraphLike = Union[ConjunctiveLike, Graph]
Expand All @@ -23,3 +23,4 @@ class SHACLExecutor:
debug: bool = False
sparql_mode: bool = False
max_validation_depth: int = 15
focus_nodes: Optional[List[URIRef]] = None
16 changes: 16 additions & 0 deletions pyshacl/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,22 @@ def validate(
return True, []
else:
self.logger.debug(f"Running evaluation of Shape {str(self)}")

if executor.focus_nodes is not None and len(executor.focus_nodes) > 0:
filtered_focus_nodes = []
for f in focus:
if f in executor.focus_nodes:
filtered_focus_nodes.append(f)
len_orig_focus = len(focus)
len_filtered_focus = len(filtered_focus_nodes)
if len_filtered_focus < 1:
self.logger.debug(f"Skipping shape {str(self)} because specified focus nodes are not targeted.")
return True, []
elif len_filtered_focus != len_orig_focus:
self.logger.debug(
f"Filtered focus nodes based on focus_nodes option. Only {len_filtered_focus} of {len_orig_focus} focus nodes remain."
)
focus = filtered_focus_nodes
t1 = ct1 = 0.0 # prevent warnings about use-before-assign
collect_stats = bool(executor.debug)

Expand Down
27 changes: 27 additions & 0 deletions pyshacl/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def _load_default_options(cls, options_dict: dict):
options_dict.setdefault('allow_warnings', False)
options_dict.setdefault('sparql_mode', False)
options_dict.setdefault('max_validation_depth', 15)
options_dict.setdefault('focus_nodes', None)
if 'logger' not in options_dict:
options_dict['logger'] = logging.getLogger(__name__)
if options_dict['debug']:
Expand Down Expand Up @@ -230,6 +231,7 @@ def make_executor(self) -> SHACLExecutor:
iterate_rules=bool(self.options.get("iterate_rules", False)),
sparql_mode=bool(self.options.get("sparql_mode", False)),
max_validation_depth=self.options.get("max_validation_depth", 15),
focus_nodes=self.options.get("focus_nodes", None),
debug=self.debug,
)

Expand Down Expand Up @@ -275,6 +277,27 @@ def run(self):
self._target_graph = the_target_graph

shapes = self.shacl_graph.shapes # This property getter triggers shapes harvest.
limit_focus_nodes = self.options.get("focus_nodes", None)
if limit_focus_nodes is not None and len(limit_focus_nodes) > 0:
# Expand any CURIEs in the focus_nodes list
expanded_focus_nodes = []
for f in limit_focus_nodes:
f_lower = f.lower()
if (
f_lower.startswith("http:")
or f_lower.startswith("https:")
or f_lower.startswith("urn:")
or f_lower.startswith("file:")
):
expanded_focus_nodes.append(URIRef(f))
else:
try:
expanded_focus_node = self.target_graph.namespace_manager.expand_curie(f)
except ValueError:
expanded_focus_node = URIRef(f)
expanded_focus_nodes.append(expanded_focus_node)
self.options["focus_nodes"] = expanded_focus_nodes

executor = self.make_executor()
if executor.advanced_mode:
self.logger.debug("Activating SHACL-AF Features.")
Expand Down Expand Up @@ -406,6 +429,7 @@ def validate(
allow_warnings: Optional[bool] = False,
max_validation_depth: Optional[int] = None,
sparql_mode: Optional[bool] = False,
focus_nodes: Optional[List[Union[str | URIRef]]] = None,
ashleysommer marked this conversation as resolved.
Show resolved Hide resolved
**kwargs,
):
"""
Expand Down Expand Up @@ -434,6 +458,8 @@ def validate(
:type max_validation_depth: int | None
:param sparql_mode: Treat the DataGraph as a SPARQL endpoint, validate the graph at the SPARQL endpoint.
:type sparql_mode: bool | None
:param focus_nodes: A list of IRIs to validate only those nodes.
:type focus_nodes: list | None
:param kwargs:
:return:
"""
Expand Down Expand Up @@ -532,6 +558,7 @@ def validate(
'use_js': use_js,
'sparql_mode': sparql_mode,
'logger': log,
'focus_nodes': focus_nodes,
}
if max_validation_depth is not None:
validator_options_dict['max_validation_depth'] = max_validation_depth
Expand Down
Loading