Skip to content

Commit

Permalink
ExpressionConstraintComponent is implemented!
Browse files Browse the repository at this point in the history
- Use your previously defined SHACL Functions to express complex constraints
- Added DASH-tests for ExpressionConstraintComponent
- Added advanced tests for ExpressionConstraintComponent, SHACLRules, and SHACLFunctions.
- New Advanced features example, showcasing ExpressionConstraint and others features
- Allow sh:message to be attached to an expression block, without breaking its functionality
- A SHACL Function within a SHACL Expression now must be a list-valued property.
- Refactored node-expression and path-expression methods to be common and reusable code
- Re-black and isort all source files
  • Loading branch information
ashleysommer committed Aug 19, 2021
1 parent 137d8a0 commit d7da1f1
Show file tree
Hide file tree
Showing 17 changed files with 822 additions and 288 deletions.
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
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/).

## [0.16.1] - 2021-08-20

### Added
- [ExpressionConstraintComponent](https://www.w3.org/TR/shacl-af/#ExpressionConstraintComponent) is implemented!
- Use your previously defined SHACL Functions to express complex constraints
- Added DASH-tests for ExpressionConstraintComponent
- Added advanced tests for ExpressionConstraintComponent, SHACLRules, and SHACLFunctions.
- New Advanced features example, showcasing ExpressionConstraint and others features

### Changed
- Allow sh:message to be attached to an expression block, without breaking its functionality
- A SHACL Function within a SHACL Expression now must be a list-valued property.
- Refactored node-expression and path-expression methods to be common and reusable code
- Re-black and isort all source files


## [0.16.0] - 2021-08-19

### Changed
Expand Down Expand Up @@ -772,7 +788,8 @@ just leaves the files open. Now it is up to the command-line client to close the

- Initial version, limited functionality

[Unreleased]: https://github.com/RDFLib/pySHACL/compare/v0.16.0...HEAD
[Unreleased]: https://github.com/RDFLib/pySHACL/compare/v0.16.1...HEAD
[0.16.1]: https://github.com/RDFLib/pySHACL/compare/v0.16.0...v0.16.1
[0.16.0]: https://github.com/RDFLib/pySHACL/compare/v0.15.0...v0.16.0
[0.15.0]: https://github.com/RDFLib/pySHACL/compare/v0.14.5...v0.15.0
[0.14.5]: https://github.com/RDFLib/pySHACL/compare/v0.14.4...v0.14.5
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors:
given-names: "Nicholas"
orcid: "http://orcid.org/0000-0002-8742-7730"
title: "pySHACL"
version: 0.16.0
version: 0.16.1
doi: 10.5281/zenodo.4750840
license: Apache-2.0
date-released: 2021-07-20
Expand Down
2 changes: 1 addition & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
### [Expression Constraints][AFExpression]
| Path | Link | Status | Comments |
|:---------- |:------: |:-------------: |:------: |
| `sh:ExpressionConstraintComponent` | [][AFExpression] | ![status-missing] | |
| `sh:ExpressionConstraintComponent` | [][AFExpression] | ![status-complete] | |

### [SHACL Rules](https://www.w3.org/TR/shacl-af/#rules)
| Parameter | Link | Status | Comments |
Expand Down
138 changes: 138 additions & 0 deletions examples/advanced.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""\
A cool test that combines a bunch of SHACL-AF features, including:
SHACL Functions (implemented as SPARQL functions)
SHACL Rules
Node Expressions
Expression Constraint
"""

from pyshacl import validate
from rdflib import Graph

shacl_file = '''\
# prefix: ex
@prefix ex: <http://datashapes.org/shasf/tests/expression/advanced.test.shacl#> .
@prefix exOnt: <http://datashapes.org/shasf/tests/expression/advanced.test.ont#> .
@prefix exData: <http://datashapes.org/shasf/tests/expression/advanced.test.data#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://datashapes.org/shasf/tests/expression/advanced.test.shacl>
rdf:type owl:Ontology ;
rdfs:label "Test of advanced features" ;
.
ex:concat
a sh:SPARQLFunction ;
rdfs:comment "Concatenates strings $op1 and $op2." ;
sh:parameter [
sh:path ex:op1 ;
sh:datatype xsd:string ;
sh:description "The first string" ;
] ;
sh:parameter [
sh:path ex:op2 ;
sh:datatype xsd:string ;
sh:description "The second string" ;
] ;
sh:returnType xsd:string ;
sh:select """
SELECT ?result
WHERE {
BIND(CONCAT(STR(?op1),STR(?op2)) AS ?result) .
}
""" .
ex:strlen
a sh:SPARQLFunction ;
rdfs:comment "Returns length of the given string." ;
sh:parameter [
sh:path ex:op1 ;
sh:datatype xsd:string ;
sh:description "The string" ;
] ;
sh:returnType xsd:integer ;
sh:select """
SELECT ?result
WHERE {
BIND(STRLEN(?op1) AS ?result) .
}
""" .
ex:lessThan
a sh:SPARQLFunction ;
rdfs:comment "Returns True if op1 < op2." ;
sh:parameter [
sh:path ex:op1 ;
sh:datatype xsd:integer ;
sh:description "The first int" ;
] ;
sh:parameter [
sh:path ex:op2 ;
sh:datatype xsd:integer ;
sh:description "The second int" ;
] ;
sh:returnType xsd:boolean ;
sh:select """
SELECT ?result
WHERE {
BIND(IF(?op1 < ?op2, true, false) AS ?result) .
}
""" .
ex:PersonExpressionShape
a sh:NodeShape ;
sh:targetClass exOnt:Person ;
sh:expression [
sh:message "Person's firstName and lastName together should be less than 35 chars long." ;
ex:lessThan (
[ ex:strlen (
[ ex:concat ( [ sh:path exOnt:firstName] [ sh:path exOnt:lastName ] ) ] )
]
35 );
] .
ex:PersonRuleShape
a sh:NodeShape ;
sh:targetClass exOnt:Administrator ;
sh:message "An administrator is a person too." ;
sh:rule [
a sh:TripleRule ;
sh:subject sh:this ;
sh:predicate rdf:type ;
sh:object exOnt:Person ;
] .
'''

data_graph = '''
# prefix: ex
@prefix ex: <http://datashapes.org/shasf/tests/expression/advanced.test.data#> .
@prefix exOnt: <http://datashapes.org/shasf/tests/expression/advanced.test.ont#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:Kate
rdf:type exOnt:Person ;
exOnt:firstName "Kate" ;
exOnt:lastName "Jones" ;
.
ex:Jenny
rdf:type exOnt:Administrator ;
exOnt:firstName "Jennifer" ;
exOnt:lastName "Wolfeschlegelsteinhausenbergerdorff" ;
.
'''

if __name__ == "__main__":
d = Graph().parse(data=data_graph, format="turtle")
s = Graph().parse(data=shacl_file, format="turtle")
conforms, report, message = validate(d, shacl_graph=s, advanced=True, debug=False)
print(message)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "pyshacl"
version = "0.16.0"
version = "0.16.1"
# Don't forget to change the version number in __init__.py and CITATION.cff along with this one
description = "Python SHACL Validator"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion pyshacl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


# version compliant with https://www.python.org/dev/peps/pep-0440/
__version__ = '0.16.0'
__version__ = '0.16.1'
# Don't forget to change the version number in pyproject.toml and CITATION.cff along with this one

__all__ = ['validate', 'Validator', '__version__', 'Shape', 'ShapesGraph']
93 changes: 93 additions & 0 deletions pyshacl/constraints/advanced/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#
"""
SHACL-AF Advanced Constraints
https://www.w3.org/TR/shacl-af/#ExpressionConstraintComponent
"""
import typing

from typing import Dict, List

from rdflib import Literal

from pyshacl.constraints.constraint_component import ConstraintComponent
from pyshacl.consts import SH, SH_message
from pyshacl.errors import ConstraintLoadError
from pyshacl.helper.expression_helper import nodes_from_node_expression
from pyshacl.pytypes import GraphLike


SH_expression = SH.expression
SH_ExpressionConstraintComponent = SH.ExpressionConstraintComponent

if typing.TYPE_CHECKING:
from pyshacl.shape import Shape


class ExpressionConstraint(ConstraintComponent):

shacl_constraint_component = SH_ExpressionConstraintComponent

def __init__(self, shape: 'Shape'):
super(ExpressionConstraint, self).__init__(shape)
self.expr_nodes = list(self.shape.objects(SH_expression))
if len(self.expr_nodes) < 1:
raise ConstraintLoadError(
"ExpressionConstraintComponent must have at least one sh:expression predicate.",
"https://www.w3.org/TR/shacl-af/#ExpressionConstraintComponent",
)

@classmethod
def constraint_parameters(cls):
return [SH_expression]

@classmethod
def constraint_name(cls):
return "ExpressionConstraintComponent"

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[Literal]:
return [Literal("Expression evaluation generated constraint did not return true.")]

def evaluate(self, data_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
:type data_graph: rdflib.Graph
:type focus_value_nodes: dict
:type _evaluation_path: list
"""
reports = []
non_conformant = False
for n in self.expr_nodes:
_n, _r = self._evaluate_expression(data_graph, focus_value_nodes, n)
non_conformant = non_conformant or _n
reports.extend(_r)
return (not non_conformant), reports

def _evaluate_expression(self, data_graph, f_v_dict, expr):
reports = []
non_conformant = False
messages = list(self.shape.sg.objects(expr, SH_message))
if len(messages):
messages = [next(iter(messages))]
else:
messages = None
for f, value_nodes in f_v_dict.items():
for v in value_nodes:
try:
n_set = nodes_from_node_expression(expr, v, data_graph, self.shape.sg)
if (
isinstance(n_set, (list, set))
and len(n_set) == 1
and next(iter(n_set)) in (Literal(True), True)
):
...
else:
non_conformant = non_conformant or True
reports.append(
self.make_v_result(
data_graph, f, value_node=v, source_constraint=expr, extra_messages=messages
)
)
except Exception as e:
print(e)
raise
return non_conformant, reports
31 changes: 16 additions & 15 deletions pyshacl/constraints/constraint_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple

import rdflib

from rdflib import BNode, Literal, URIRef

from pyshacl.consts import (
Expand Down Expand Up @@ -39,6 +37,8 @@


if TYPE_CHECKING:
from rdflib.term import Identifier

from pyshacl.shape import Shape
from pyshacl.shapes_graph import ShapesGraph

Expand Down Expand Up @@ -113,9 +113,9 @@ def recursion_triggers(self, _evaluation_path):
def make_v_result_description(
self,
datagraph: GraphLike,
focus_node: 'rdflib.term.Identifier',
focus_node: 'Identifier',
severity: URIRef,
value_node: Optional['rdflib.term.Identifier'],
value_node: Optional['Identifier'],
messages: List[str],
result_path=None,
constraint_component=None,
Expand Down Expand Up @@ -195,27 +195,28 @@ def make_v_result_description(
def make_v_result(
self,
datagraph: GraphLike,
focus_node: 'rdflib.term.Identifier',
value_node: Optional['rdflib.term.Identifier'] = None,
result_path=None,
constraint_component=None,
source_constraint=None,
focus_node: 'Identifier',
value_node: Optional['Identifier'] = None,
result_path: Optional['Identifier'] = None,
constraint_component: Optional['Identifier'] = None,
source_constraint: Optional['Identifier'] = None,
extra_messages: Optional[Iterable] = None,
bound_vars=None,
):
"""
:param datagraph:
:type datagraph: rdflib.Graph | rdflib.ConjunctiveGraph | rdflib.Dataset
:param focus_node:
:type focus_node: rdflib.term.Identifier
:type focus_node: Identifier
:param value_node:
:type value_node: rdflib.term.Identifier | None
:type value_node: Identifier | None
:param result_path:
:param bound_vars:
:type result_path: Identifier | None
:param constraint_component:
:param source_constraint:
:param extra_messages:
:type extra_messages: collections.abc.Iterable | None
:param bound_vars:
:return:
"""
constraint_component = constraint_component or self.shacl_constraint_component
Expand All @@ -228,13 +229,13 @@ def make_v_result(
r_triples.append((r_node, SH_sourceShape, (sg, self.shape.node)))
r_triples.append((r_node, SH_resultSeverity, severity))
r_triples.append((r_node, SH_focusNode, (datagraph or sg, focus_node)))
if value_node:
if value_node is not None:
r_triples.append((r_node, SH_value, (datagraph, value_node)))
if result_path is None and self.shape.is_property_shape:
result_path = self.shape.path()
if result_path:
if result_path is not None:
r_triples.append((r_node, SH_resultPath, (sg, result_path)))
if source_constraint:
if source_constraint is not None:
r_triples.append((r_node, SH_sourceConstraint, (sg, source_constraint)))
messages = list(self.shape.message)
if extra_messages:
Expand Down
Loading

0 comments on commit d7da1f1

Please sign in to comment.