Skip to content

Commit

Permalink
added autogenerated default messages for almost all Core constraints
Browse files Browse the repository at this point in the history
Fixed missing sh:value and sh:focusNode on validion results in validation reports when datagraph is a multigraph.
Bump to v0.12.1
Fixed some travis deployment weirdness
  • Loading branch information
ashleysommer committed Jul 22, 2020
1 parent 730b682 commit cf6df94
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 61 deletions.
6 changes: 5 additions & 1 deletion .travis.yml
Expand Up @@ -27,7 +27,9 @@ matrix:
- env: TOX_ENV=lint
python: 3.6
name: "Python 3.6 Linter checks"

- env: TOX_ENV=py36,type-checking,lint DEPLOY=true
python: 3.6
name: "Aggregate or all above, and deploy"

install:
- pip3 install --upgrade pip "poetry>=1.0.9" tox
Expand All @@ -43,6 +45,8 @@ deploy:
on:
tags: true
python: 3.6
condition: $DEPLOY = true
branch: release
distributions: "sdist bdist_wheel"
skip_existing: true

29 changes: 28 additions & 1 deletion CHANGELOG.md
Expand Up @@ -4,6 +4,32 @@ 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.12.1] - 2020-07-22

## Added
- All SHACL Core constraints now have their own autogenerated sh:message.
- This is used as a fallback when your Shape does not provide its own sh:message
- See the new sh:resultMessage entries in the Validation Report output
- These are hopefully more human-readable than the other fields of the Validation Report results

- Added a copy of the implementation of the new 'Memory2' rdflib triplestore backend.
- This when using Python 3.6 or above, this is faster than the default 'IOMemory' store by:
- 10.3% when benchmarking validation with no inferencing
- 17% when benchmarking validation with rdfs inferencing
- 19.5% when benchmarking validation with rdfs+owlrl inferencing

## Changed
- PySHACL is now categorised as **Production/Stable**.
- This marks a level of maturity in PySHACL we are happy to no longer consider a beta
- A v1.0.0 might be coming soon, but its just a version number, doesn't mean anything special
- Changed default rdflib triplestore backend to 'Memory2' as above.
- Tiny optimisations in the way sh:message items are added to a validation report graph.

## Fixed
- Regression since v0.11.0, sh:value and sh:focusNode from the datagraph were not included in the validation report
graph if the datagraph was of type rdflib.ConjunctiveGraph or rdflib.Dataset.


## [0.12.0] - 2020-07-10

### Removed
Expand Down Expand Up @@ -554,7 +580,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.12.0...HEAD
[Unreleased]: https://github.com/RDFLib/pySHACL/compare/v0.12.1...HEAD
[0.12.1]: https://github.com/RDFLib/pySHACL/compare/v0.12.0...v0.12.1
[0.12.0]: https://github.com/RDFLib/pySHACL/compare/v0.11.6.post1...v0.12.0
[0.11.6.post1]: https://github.com/RDFLib/pySHACL/compare/v0.11.6...v0.11.6.post1
[0.11.6]: https://github.com/RDFLib/pySHACL/compare/v0.11.5...v0.11.6
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"

[tool.poetry]
name = "pyshacl"
version = "0.12.0"
version = "0.12.1"
description = "Python SHACL Validator"
license = "Apache-2.0"
authors = [
Expand All @@ -15,7 +15,7 @@ repository = "https://github.com/RDFLib/pySHACL"
homepage = "https://github.com/RDFLib/pySHACL"
keywords = ["Linked Data", "Semantic Web", "RDF", "Python", "SHACL", "Shapes", "Schema", "Validate"]
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Topic :: Utilities",
"Intended Audience :: Developers",
"Natural Language :: English",
Expand Down
2 changes: 1 addition & 1 deletion pyshacl/__init__.py
Expand Up @@ -4,6 +4,6 @@


# version compliant with https://www.python.org/dev/peps/pep-0440/
__version__ = '0.12.0'
__version__ = '0.12.1'

__all__ = ['validate', 'Validator', '__version__']
55 changes: 31 additions & 24 deletions pyshacl/constraints/constraint_component.py
Expand Up @@ -67,9 +67,8 @@ def shacl_constraint_class(cls):
def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
raise NotImplementedError() # pragma: no cover

def make_generic_message(self):
print(self)
return None # pragma: no cover
def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[Literal]:
return []

def __str__(self):
c_name = str(self.__class__.__name__)
Expand Down Expand Up @@ -103,6 +102,7 @@ def make_v_result_description(
focus_node: 'rdflib.term.Identifier',
severity: URIRef,
value_node: Optional['rdflib.term.Identifier'],
messages: List[str],
result_path=None,
constraint_component=None,
source_constraint=None,
Expand All @@ -118,6 +118,8 @@ def make_v_result_description(
:type value_node: rdflib.URIRef
:param value_node:
:type value_node: rdflib.term.Identifier | None
:param messages:
:type messages: List[str]
:param result_path:
:param constraint_component:
:param source_constraint:
Expand All @@ -131,7 +133,7 @@ def make_v_result_description(
if severity == SH_Violation:
severity_desc = "Constraint Violation"
else:
severity_desc = "Constraint Report"
severity_desc = "Validation Result"
source_shape_text = stringify_node(sg, self.shape.node)
severity_node_text = stringify_node(sg, severity)
focus_node_text = stringify_node(datagraph or sg, focus_node)
Expand All @@ -156,13 +158,13 @@ def make_v_result_description(
desc += "\tSource Constraint: {}\n".format(sc_text)
if extra_messages:
for m in iter(extra_messages):
if m in self.shape.message:
if m in messages:
continue
if isinstance(m, Literal):
desc += "\tMessage: {}\n".format(str(m.value))
else: # pragma: no cover
desc += "\tMessage: {}\n".format(str(m))
for m in self.shape.message:
for m in messages:
if isinstance(m, Literal):
desc += "\tMessage: {}\n".format(str(m.value))
else: # pragma: no cover
Expand Down Expand Up @@ -195,37 +197,42 @@ def make_v_result(
"""
constraint_component = constraint_component or self.shacl_constraint_class()
severity = self.shape.severity
sg = self.shape.sg.graph
r_triples = list()
r_node = BNode()
r_triples.append((r_node, RDF_type, SH_ValidationResult))
r_triples.append((r_node, SH_sourceConstraintComponent, ('S', constraint_component)))
r_triples.append((r_node, SH_sourceShape, ('S', self.shape.node)))
r_triples.append((r_node, SH_sourceConstraintComponent, (sg, constraint_component)))
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, ('D', focus_node)))
r_triples.append((r_node, SH_focusNode, (datagraph or sg, focus_node)))
if value_node:
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:
r_triples.append((r_node, SH_resultPath, (sg, result_path)))
if source_constraint:
r_triples.append((r_node, SH_sourceConstraint, (sg, source_constraint)))
messages = list(self.shape.message)
if extra_messages:
for m in iter(extra_messages):
if m in messages:
continue
r_triples.append((r_node, SH_resultMessage, m))
elif not messages:
messages = self.make_generic_messages(datagraph, focus_node, value_node) or messages
for m in messages:
r_triples.append((r_node, SH_resultMessage, m))
desc = self.make_v_result_description(
datagraph,
focus_node,
severity,
value_node,
messages,
result_path=result_path,
constraint_component=constraint_component,
source_constraint=source_constraint,
extra_messages=extra_messages,
)
if value_node:
r_triples.append((r_node, SH_value, ('D', value_node)))
if result_path is None and self.shape.is_property_shape:
result_path = self.shape.path()
if result_path:
r_triples.append((r_node, SH_resultPath, ('S', result_path)))
if source_constraint:
r_triples.append((r_node, SH_sourceConstraint, ('S', source_constraint)))
for m in self.shape.message:
r_triples.append((r_node, SH_resultMessage, m))
if extra_messages:
for m in iter(extra_messages):
if m in self.shape.message:
continue
r_triples.append((r_node, SH_resultMessage, m))
self.shape.logger.debug(desc)
return desc, r_node, r_triples
29 changes: 25 additions & 4 deletions pyshacl/constraints/core/cardinality_constraints.py
Expand Up @@ -11,6 +11,7 @@
from pyshacl.consts import SH
from pyshacl.errors import ConstraintLoadError
from pyshacl.pytypes import GraphLike
from pyshacl.rdfutil import stringify_node


XSD_integer = XSD.term('integer')
Expand Down Expand Up @@ -72,6 +73,17 @@ def constraint_name(cls):
def shacl_constraint_class(cls):
return SH_MinCountConstraintComponent

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[Literal]:
p = self.shape.path()
if p:
p = stringify_node(self.shape.sg.graph, p)
m = "Less than {} values on {}->{}".format(
str(self.min_count.value), stringify_node(datagraph, focus_node), p
)
else:
m = "Less than {} values on {}".format(str(self.min_count.value), stringify_node(datagraph, focus_node))
return [Literal(m)]

def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
:type target_graph: rdflib.Graph
Expand All @@ -86,8 +98,7 @@ def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation
non_conformant = False

for f, value_nodes in focus_value_nodes.items():
flag = len(value_nodes) >= min_count
if not flag:
if not len(value_nodes) >= min_count:
non_conformant = True
rept = self.make_v_result(target_graph, f)
reports.append(rept)
Expand Down Expand Up @@ -144,6 +155,17 @@ def constraint_name(cls):
def shacl_constraint_class(cls):
return SH_MaxCountConstraintComponent

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[Literal]:
p = self.shape.path()
if p:
p = stringify_node(self.shape.sg.graph, p)
m = "More than {} values on {}->{}".format(
str(self.max_count.value), stringify_node(datagraph, focus_node), p
)
else:
m = "More than {} values on {}".format(str(self.max_count.value), stringify_node(datagraph, focus_node))
return [Literal(m)]

def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
:type target_graph: rdflib.Graph
Expand All @@ -155,8 +177,7 @@ def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation
non_conformant = False

for f, value_nodes in focus_value_nodes.items():
flag = len(value_nodes) <= max_count
if not flag:
if not len(value_nodes) <= max_count:
non_conformant = True
rept = self.make_v_result(target_graph, f)
reports.append(rept)
Expand Down
32 changes: 32 additions & 0 deletions pyshacl/constraints/core/logical_constraints.py
Expand Up @@ -5,10 +5,13 @@
from typing import Dict, List
from warnings import warn

import rdflib

from pyshacl.constraints.constraint_component import ConstraintComponent
from pyshacl.consts import SH
from pyshacl.errors import ConstraintLoadError, ReportableRuntimeError, ShapeRecursionWarning, ValidationFailure
from pyshacl.pytypes import GraphLike
from pyshacl.rdfutil import stringify_node


SH_not = SH.term('not')
Expand Down Expand Up @@ -58,6 +61,12 @@ def constraint_name(cls):
def shacl_constraint_class(cls):
return SH_NotConstraintComponent

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[rdflib.Literal]:
m = "Node {} conforms to shape {}".format(
stringify_node(datagraph, value_node), stringify_node(self.shape.sg.graph, self.not_list[0])
)
return [rdflib.Literal(m)]

def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
Expand Down Expand Up @@ -135,6 +144,13 @@ def constraint_name(cls):
def shacl_constraint_class(cls):
return SH_AndConstraintComponent

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[rdflib.Literal]:
and_list = " , ".join(
stringify_node(self.shape.sg.graph, a_c) for a in self.and_list for a_c in self.shape.sg.graph.items(a)
)
m = "Node {} does not conforms to all shapes in {}".format(stringify_node(datagraph, value_node), and_list)
return [rdflib.Literal(m)]

def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
Expand Down Expand Up @@ -217,6 +233,15 @@ def constraint_name(cls):
def shacl_constraint_class(cls):
return SH_OrConstraintComponent

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[rdflib.Literal]:
or_list = " , ".join(
stringify_node(self.shape.sg.graph, o_c) for o in self.or_list for o_c in self.shape.sg.graph.items(o)
)
m = "Node {} does not conform to one or more shapes in {}".format(
stringify_node(datagraph, value_node), or_list
)
return [rdflib.Literal(m)]

def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
:type target_graph: rdflib.Graph
Expand Down Expand Up @@ -298,6 +323,13 @@ def constraint_name(cls):
def shacl_constraint_class(cls):
return SH_XoneConstraintComponent

def make_generic_messages(self, datagraph: GraphLike, focus_node, value_node) -> List[rdflib.Literal]:
xone_list = " , ".join(
stringify_node(self.shape.sg.graph, a_c) for a in self.xone_nodes for a_c in self.shape.sg.graph.items(a)
)
m = "Node {} does not conform exactly one shape in {}".format(stringify_node(datagraph, value_node), xone_list)
return [rdflib.Literal(m)]

def evaluate(self, target_graph: GraphLike, focus_value_nodes: Dict, _evaluation_path: List):
"""
Expand Down

0 comments on commit cf6df94

Please sign in to comment.