Skip to content

Commit

Permalink
Fixed Issue [#32](#32)
Browse files Browse the repository at this point in the history
Stringification of Focus Node, and Value Node in the results text string now works correctly
- This is an old bug, that has been around since the first versions of pySHACL
- Manifests when the DataGraph is a different graph than the ShapesGraph
- Recent change from using Graphs by default to using Datasets by default helped to expose this bug
- Thanks to @jameshowison for reporting the bug
Stringification of a blank node now operates on a rdflib.Graph only, rather than a Dataset.
- Added mechanism to extract the correct named graph from a dataset when stringifying a blank node.
Added a workaround for a json-ld loader bug where the namespace_manager for named graphs within a conjunctive graph is set to the parent conjunctive graph.
- This necessary workaround was exposed only after changing the blank node stringification above. (Fixing one bug exposed another bug!)
  • Loading branch information
ashleysommer committed Oct 21, 2019
1 parent aa651b4 commit f479b1e
Show file tree
Hide file tree
Showing 18 changed files with 218 additions and 88 deletions.
30 changes: 26 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,37 @@ 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.11.2] - 2019-17-10
## [0.11.3] - 2019-21-10

### Fixed
- Fixed Issue [#032](https://github.com/RDFLib/pySHACL/issues/32)
- Stringification of Focus Node, and Value Node in the results text string now works correctly
- This is an old bug, that has been around since the first versions of pySHACL
- Manifests when the DataGraph is a different graph than the ShapesGraph
- Recent change from using Graphs by default to using Datasets by default helped to expose this bug
- Thanks to @jameshowison for reporting the bug

### Changed
- Bumped min OWL-RL version to 5.2.1 to bring in some new bugfixes
- Corrected some tiny typos in readme
- Stringification of a blank node now operates on a rdflib.Graph only, rather than a Dataset.
- Added mechanism to extract the correct named graph from a dataset when stringifying a blank node.
- Added a workaround for a json-ld loader bug where the namespace_manager for named graphs within a conjunctive graph
is set to the parent conjunctive graph.
- This necessary workaround was exposed only after changing the blank node stringification above.
(Fixing one bug exposed another bug!)

### Announcement
- **This is the final version with Python v3.5 support**
- Versions 0.12.0 and above will have newer package management and dependency management, and will
require Python v3.6+.


## [0.11.2] - 2019-17-10

### Changed
- Bumped min OWL-RL version to 5.2.1 to bring in some new bugfixes
- Corrected some tiny typos in readme


## [0.11.1.post1] - 2019-11-10

### Fixed
Expand Down Expand Up @@ -453,7 +474,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.11.2...HEAD
[Unreleased]: https://github.com/RDFLib/pySHACL/compare/v0.11.3...HEAD
[0.11.3]: https://github.com/RDFLib/pySHACL/compare/v0.11.2...v0.11.3
[0.11.2]: https://github.com/RDFLib/pySHACL/compare/v0.11.1.post1...v0.11.2
[0.11.1.post1]: https://github.com/RDFLib/pySHACL/compare/v0.11.1...v0.11.1.post1
[0.11.1]: https://github.com/RDFLib/pySHACL/compare/v0.11.0...v0.11.1
Expand Down
2 changes: 1 addition & 1 deletion pyshacl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from pyshacl.validate import validate, Validator

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

__all__ = ['validate', 'Validator', '__version__']
36 changes: 31 additions & 5 deletions pyshacl/constraints/constraint_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,21 @@ def shacl_constraint_class(cls):
def evaluate(self, target_graph, focus_value_nodes):
return NotImplementedError() # pragma: no cover

def make_v_result_description(self, severity, focus_node, value_node=None, result_path=None,
def make_v_result_description(self, datagraph, focus_node, severity, value_node=None, result_path=None,
constraint_component=None, source_constraint=None, extra_messages=None):
"""
:param datagraph:
:type datagraph: rdflib.Graph | rdflib.Dataset
:param focus_node:
:type focus_node: rdflib.term.Identifier
:param value_node:
:type value_node: rdflib.term.Identifier | None
:param result_path:
:param constraint_component:
:param source_constraint:
:param extra_messages:
:return:
"""
sg = self.shape.sg.graph
constraint_component = constraint_component or self.shacl_constraint_class()
constraint_name = self.constraint_name()
Expand All @@ -51,14 +64,14 @@ def make_v_result_description(self, severity, focus_node, value_node=None, resul
else:
severity_desc = "Constraint Report"
source_shape_text = stringify_node(sg, self.shape.node)
focus_node_text = stringify_node(sg, focus_node)
severity_node_text = stringify_node(sg, severity)
focus_node_text = stringify_node(datagraph or sg, focus_node)
desc = "{} in {} ({}):\n\tSeverity: {}\n\tSource Shape: {}\n\tFocus Node: {}\n"\
.format(severity_desc, constraint_name,
str(constraint_component),
severity_node_text, source_shape_text, focus_node_text)
if value_node is not None:
val_node_string = stringify_node(sg, value_node)
val_node_string = stringify_node(datagraph or sg, value_node)
desc += "\tValue Node: {}\n".format(val_node_string)
if result_path is None and self.shape.is_property_shape:
result_path = self.shape.path()
Expand All @@ -83,9 +96,22 @@ def make_v_result_description(self, severity, focus_node, value_node=None, resul
desc += "\tMessage: {}\n".format(str(m))
return desc

def make_v_result(self, focus_node, value_node=None, result_path=None,
def make_v_result(self, datagraph, focus_node, value_node=None, result_path=None,
constraint_component=None, source_constraint=None,
extra_messages=None):
"""
:param datagraph:
:type datagraph: rdflib.Graph | rdflib.Dataset
:param focus_node:
:type focus_node: rdflib.term.Identifier
:param value_node:
:type value_node: rdflib.term.Identifier | None
:param result_path:
:param constraint_component:
:param source_constraint:
:param extra_messages:
:return:
"""
constraint_component = constraint_component or self.shacl_constraint_class()
severity = self.shape.severity
r_triples = list()
Expand All @@ -96,7 +122,7 @@ def make_v_result(self, focus_node, value_node=None, result_path=None,
r_triples.append((r_node, SH_resultSeverity, severity))
r_triples.append((r_node, SH_focusNode, ('D', focus_node)))
desc = self.make_v_result_description(
severity, focus_node, value_node,
datagraph, focus_node, severity, value_node,
result_path=result_path, constraint_component=constraint_component,
source_constraint=source_constraint, extra_messages=extra_messages)
if value_node:
Expand Down
4 changes: 2 additions & 2 deletions pyshacl/constraints/core/cardinality_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def evaluate(self, target_graph, focus_value_nodes):
flag = len(value_nodes) >= min_count
if not flag:
non_conformant = True
rept = self.make_v_result(f)
rept = self.make_v_result(target_graph, f)
reports.append(rept)
return (not non_conformant), reports

Expand Down Expand Up @@ -146,7 +146,7 @@ def evaluate(self, target_graph, focus_value_nodes):
flag = len(value_nodes) <= max_count
if not flag:
non_conformant = True
rept = self.make_v_result(f)
rept = self.make_v_result(target_graph, f)
reports.append(rept)
return (not non_conformant), reports

8 changes: 4 additions & 4 deletions pyshacl/constraints/core/logical_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _evaluate_not_constraint(self, not_c, target_graph, f_v_dict):
if _is_conform:
# in this case, we _dont_ want to conform!
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -162,7 +162,7 @@ def _evaluate_and_constraint(self, and_c, target_graph, f_v_dict):
passed_all = passed_all and _is_conform
if not passed_all:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -240,7 +240,7 @@ def _evaluate_or_constraint(self, or_c, target_graph, f_v_dict):
passed_any = passed_any or _is_conform
if not passed_any:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -319,7 +319,7 @@ def _evaluate_xone_constraint(self, xone_c, target_graph, f_v_dict):
passed_count += 1
if not (passed_count == 1):
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

16 changes: 8 additions & 8 deletions pyshacl/constraints/core/other_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def evaluate(self, target_graph, focus_value_nodes):
for v in value_nodes:
if v not in in_vals:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return (not non_conformant), reports

Expand Down Expand Up @@ -159,7 +159,7 @@ def evaluate(self, target_graph, focus_value_nodes):
elif p in working_paths:
continue
non_conformant = True
rept = self.make_v_result(f, value_node=o, result_path=p)
rept = self.make_v_result(target_graph, f, value_node=o, result_path=p)
reports.append(rept)
return (not non_conformant), reports

Expand Down Expand Up @@ -205,12 +205,12 @@ def evaluate(self, target_graph, focus_value_nodes):
non_conformant = False

for hv in iter(self.has_value_set):
_nc, _r = self._evaluate_has_value(hv, focus_value_nodes)
_nc, _r = self._evaluate_has_value(target_graph, hv, focus_value_nodes)
non_conformant = non_conformant or _nc
reports.extend(_r)
return (not non_conformant), reports

def _evaluate_has_value(self, hv, f_v_dict):
def _evaluate_has_value(self, target_graph, hv, f_v_dict):
reports = []
non_conformant = False
for f, value_nodes in f_v_dict.items():
Expand All @@ -221,15 +221,15 @@ def _evaluate_has_value(self, hv, f_v_dict):
break
if not conformant:
non_conformant = True
# Note, including the value in the report generation here causes this constraint to not pass SHT validation
# though IMHO the value _should_ be included
# Note, including the value in the report generation here causes this constraint to not pass
# SHT validation, though IMHO the value _should_ be included
# if len(value_nodes) == 1:
# a_value_node = next(iter(value_nodes))
# rept = self.make_v_result(f, value_node=a_value_node)
# else:
if not self.shape.is_property_shape:
rept = self.make_v_result(f, value_node=f)
rept = self.make_v_result(target_graph, f, value_node=f)
else:
rept = self.make_v_result(f, value_node=None)
rept = self.make_v_result(target_graph, f, value_node=None)
reports.append(rept)
return non_conformant, reports
24 changes: 12 additions & 12 deletions pyshacl/constraints/core/property_pair_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ def _evaluate_propety_equals(self, eq, target_graph, f_v_dict):
else:
continue
for value_node in value_nodes_missing:
rept = self.make_v_result(f, value_node=value_node)
rept = self.make_v_result(target_graph, f, value_node=value_node)
reports.append(rept)
for compare_value in compare_values_missing:
rept = self.make_v_result(f, value_node=compare_value)
rept = self.make_v_result(target_graph, f, value_node=compare_value)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -146,7 +146,7 @@ def _evaluate_propety_disjoint(self, dj, target_graph, f_v_dict):
else:
continue
for common_node in common_nodes:
rept = self.make_v_result(f, value_node=common_node)
rept = self.make_v_result(target_graph, f, value_node=common_node)
reports.append(rept)

return non_conformant, reports
Expand Down Expand Up @@ -236,18 +236,18 @@ def _evaluate_less_than(self, lt, target_graph, f_v_dict):
compare_value = str(compare_value)
compare_is_string = True
elif isinstance(compare_value, rdflib.Literal) and\
isinstance(compare_value.value, str):
isinstance(compare_value.value, str):
compare_value = compare_value.value
compare_is_string = True
if (value_is_string and not compare_is_string) or\
(compare_is_string and not value_is_string):
non_conformant = True
rept = self.make_v_result(f, value_node=orig_value_node)
reports.append(rept)
elif not value_node < compare_value:
non_conformant = True
rept = self.make_v_result(f, value_node=orig_value_node)
reports.append(rept)
else:
continue
rept = self.make_v_result(target_graph, f, value_node=orig_value_node)
reports.append(rept)
return non_conformant, reports


Expand Down Expand Up @@ -341,10 +341,10 @@ def _evaluate_ltoe(self, lt, target_graph, f_v_dict):
if (value_is_string and not compare_is_string) or\
(compare_is_string and not value_is_string):
non_conformant = True
rept = self.make_v_result(f, value_node=orig_value_node)
reports.append(rept)
elif not value_node <= compare_value:
non_conformant = True
rept = self.make_v_result(f, value_node=orig_value_node)
reports.append(rept)
else:
continue
rept = self.make_v_result(target_graph, f, value_node=orig_value_node)
reports.append(rept)
return non_conformant, reports
6 changes: 3 additions & 3 deletions pyshacl/constraints/core/shape_based_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _evaluate_node_shape(self, node_shape, target_graph, f_v_dict):
# ignore the fails from the node, create our own fail
if (not _is_conform) or len(_r) > 0:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -275,10 +275,10 @@ def _evaluate_value_shape(self, v_shape, target_graph, f_v_dict):
raise v
if self.max_count is not None and number_conforms > self.max_count:
non_conformant = True
_r = self.make_v_result(f, constraint_component=SH_QualifiedMaxCountConstraintComponent)
_r = self.make_v_result(target_graph, f, constraint_component=SH_QualifiedMaxCountConstraintComponent)
reports.append(_r)
if self.min_count is not None and number_conforms < self.min_count:
non_conformant = True
_r = self.make_v_result(f, constraint_component=SH_QualifiedMinCountConstraintComponent)
_r = self.make_v_result(target_graph, f, constraint_component=SH_QualifiedMinCountConstraintComponent)
reports.append(_r)
return non_conformant, reports
18 changes: 11 additions & 7 deletions pyshacl/constraints/core/string_based_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def _evaluate_string_rule(self, r, target_graph, f_v_dict):
flag = len(v_string) >= min_len
if not flag:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -186,7 +186,7 @@ def _evaluate_string_rule(self, r, target_graph, f_v_dict):
flag = len(v_string) <= max_len
if not flag:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -255,7 +255,7 @@ def _evaluate_string_rule(self, r, target_graph, f_v_dict):
match = re_matcher.search(v_string)
if not match:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -332,7 +332,7 @@ def _evaluate_string_rule(self, r, target_graph, f_v_dict):
flag = True
if not flag:
non_conformant = True
rept = self.make_v_result(f, value_node=v)
rept = self.make_v_result(target_graph, f, value_node=v)
reports.append(rept)
return non_conformant, reports

Expand Down Expand Up @@ -391,7 +391,7 @@ def _evaluate_string_rule(self, is_unique_lang, target_graph, f_v_dict):
reports = []
non_conformant = False
for f, value_nodes in f_v_dict.items():
found_langs = set()
found_langs = dict()
found_duplicates = set()
for v in value_nodes:
if isinstance(v, rdflib.Literal):
Expand All @@ -400,14 +400,18 @@ def _evaluate_string_rule(self, is_unique_lang, target_graph, f_v_dict):
low_lang = str(lang).lower()
if low_lang in found_langs:
found_duplicates.add(low_lang)
found_langs.add(low_lang)
else:
found_langs[low_lang] = lang
# TODO: determine if there is duplicate matching on parts of multi-part langs.
# lang_parts = str(lang).split('-')
# first_part = lang_parts[0]
# if str(first_part).lower() in languages_need:
# flag = True
for d in iter(found_duplicates):
non_conformant = True
rept = self.make_v_result(f)
# Adding value_node here causes SHT validation to fail.
# IMHO it should be present
#rept = self.make_v_result(target_graph, f, value_node=found_langs[d])
rept = self.make_v_result(target_graph, f, value_node=None)
reports.append(rept)
return non_conformant, reports
Loading

0 comments on commit f479b1e

Please sign in to comment.