From f479b1e34315621fe82234d239a2d0c65fb8404b Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 21 Oct 2019 12:28:29 +1000 Subject: [PATCH] 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 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!) --- CHANGELOG.md | 30 +++++++++-- pyshacl/__init__.py | 2 +- pyshacl/constraints/constraint_component.py | 36 +++++++++++-- .../core/cardinality_constraints.py | 4 +- .../constraints/core/logical_constraints.py | 8 +-- pyshacl/constraints/core/other_constraints.py | 16 +++--- .../core/property_pair_constraints.py | 24 ++++----- .../core/shape_based_constraints.py | 6 +-- .../core/string_based_constraints.py | 18 ++++--- pyshacl/constraints/core/value_constraints.py | 6 +-- .../core/value_range_constraints.py | 24 ++++----- .../sparql_based_constraint_components.py | 7 ++- .../sparql/sparql_based_constraints.py | 7 ++- pyshacl/rdfutil/clone.py | 9 +++- pyshacl/rdfutil/stringify.py | 37 +++++++++---- pyshacl/validate.py | 18 ++++--- test/issues/test_014.py | 1 - test/test_extra.py | 53 ++++++++++++++++++- 18 files changed, 218 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b21a23a..7426b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/pyshacl/__init__.py b/pyshacl/__init__.py index a49ae97..d9ac297 100644 --- a/pyshacl/__init__.py +++ b/pyshacl/__init__.py @@ -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__'] diff --git a/pyshacl/constraints/constraint_component.py b/pyshacl/constraints/constraint_component.py index 0b6439c..65c491c 100644 --- a/pyshacl/constraints/constraint_component.py +++ b/pyshacl/constraints/constraint_component.py @@ -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() @@ -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() @@ -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() @@ -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: diff --git a/pyshacl/constraints/core/cardinality_constraints.py b/pyshacl/constraints/core/cardinality_constraints.py index b2c071b..9fa566d 100644 --- a/pyshacl/constraints/core/cardinality_constraints.py +++ b/pyshacl/constraints/core/cardinality_constraints.py @@ -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 @@ -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 diff --git a/pyshacl/constraints/core/logical_constraints.py b/pyshacl/constraints/core/logical_constraints.py index 404afd2..4333e94 100644 --- a/pyshacl/constraints/core/logical_constraints.py +++ b/pyshacl/constraints/core/logical_constraints.py @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/pyshacl/constraints/core/other_constraints.py b/pyshacl/constraints/core/other_constraints.py index 76429a9..e6c84d4 100644 --- a/pyshacl/constraints/core/other_constraints.py +++ b/pyshacl/constraints/core/other_constraints.py @@ -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 @@ -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 @@ -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(): @@ -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 diff --git a/pyshacl/constraints/core/property_pair_constraints.py b/pyshacl/constraints/core/property_pair_constraints.py index e51589c..7d7f077 100644 --- a/pyshacl/constraints/core/property_pair_constraints.py +++ b/pyshacl/constraints/core/property_pair_constraints.py @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/pyshacl/constraints/core/shape_based_constraints.py b/pyshacl/constraints/core/shape_based_constraints.py index a9362c0..d477d7b 100644 --- a/pyshacl/constraints/core/shape_based_constraints.py +++ b/pyshacl/constraints/core/shape_based_constraints.py @@ -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 @@ -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 diff --git a/pyshacl/constraints/core/string_based_constraints.py b/pyshacl/constraints/core/string_based_constraints.py index 12af363..34e7725 100644 --- a/pyshacl/constraints/core/string_based_constraints.py +++ b/pyshacl/constraints/core/string_based_constraints.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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): @@ -400,7 +400,8 @@ 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] @@ -408,6 +409,9 @@ def _evaluate_string_rule(self, is_unique_lang, target_graph, f_v_dict): # 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 diff --git a/pyshacl/constraints/core/value_constraints.py b/pyshacl/constraints/core/value_constraints.py index cdd4367..2212627 100644 --- a/pyshacl/constraints/core/value_constraints.py +++ b/pyshacl/constraints/core/value_constraints.py @@ -98,7 +98,7 @@ def _evaluate_class_rules(self, target_graph, f_v_dict, class_rule): break if not found: 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 @@ -165,7 +165,7 @@ def evaluate(self, target_graph, focus_value_nodes): .format(v, dtype_rule)) if not matches: 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 @@ -246,7 +246,7 @@ def evaluate(self, target_graph, focus_value_nodes): match = True 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 (not non_conformant), reports diff --git a/pyshacl/constraints/core/value_range_constraints.py b/pyshacl/constraints/core/value_range_constraints.py index 77d5e46..d80111f 100644 --- a/pyshacl/constraints/core/value_range_constraints.py +++ b/pyshacl/constraints/core/value_range_constraints.py @@ -56,12 +56,12 @@ def evaluate(self, target_graph, focus_value_nodes): non_conformant = False for m_val in self.min_vals: - _nc, _r = self._evaluate_min_rule(m_val, focus_value_nodes) + _nc, _r = self._evaluate_min_rule(m_val, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_min_rule(self, m_val, f_v_dict): + def _evaluate_min_rule(self, m_val, target_graph, f_v_dict): reports = [] non_conformant = False assert isinstance(m_val, rdflib.Literal) @@ -92,7 +92,7 @@ def _evaluate_min_rule(self, m_val, f_v_dict): "Not sure how to compare anything else.") 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 @@ -135,12 +135,12 @@ def evaluate(self, target_graph, focus_value_nodes): non_conformant = False for m_val in self.min_vals: - _nc, _r = self._evaluate_min_rule(m_val, focus_value_nodes) + _nc, _r = self._evaluate_min_rule(m_val, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_min_rule(self, m_val, f_v_dict): + def _evaluate_min_rule(self, m_val, target_graph, f_v_dict): reports = [] non_conformant = False assert isinstance(m_val, rdflib.Literal) @@ -171,7 +171,7 @@ def _evaluate_min_rule(self, m_val, f_v_dict): "Not sure how to compare anything else.") 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 @@ -215,12 +215,12 @@ def evaluate(self, target_graph, focus_value_nodes): non_conformant = False for m_val in self.max_vals: - _nc, _r = self._evaluate_max_rule(m_val, focus_value_nodes) + _nc, _r = self._evaluate_max_rule(m_val, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_max_rule(self, m_val, f_v_dict): + def _evaluate_max_rule(self, m_val, target_graph, f_v_dict): reports = [] non_conformant = False assert isinstance(m_val, rdflib.Literal) @@ -251,7 +251,7 @@ def _evaluate_max_rule(self, m_val, f_v_dict): "Not sure how to compare anything else.") 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 @@ -295,12 +295,12 @@ def evaluate(self, target_graph, focus_value_nodes): non_conformant = False for m_val in self.max_vals: - _nc, _r = self._evaluate_max_rule(m_val, focus_value_nodes) + _nc, _r = self._evaluate_max_rule(m_val, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_max_rule(self, m_val, f_v_dict): + def _evaluate_max_rule(self, m_val, target_graph, f_v_dict): reports = [] non_conformant = False assert isinstance(m_val, rdflib.Literal) @@ -331,6 +331,6 @@ def _evaluate_max_rule(self, m_val, f_v_dict): "Not sure how to compare anything else.") 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 diff --git a/pyshacl/constraints/sparql/sparql_based_constraint_components.py b/pyshacl/constraints/sparql/sparql_based_constraint_components.py index 47c7ee4..bfe76ce 100644 --- a/pyshacl/constraints/sparql/sparql_based_constraint_components.py +++ b/pyshacl/constraints/sparql/sparql_based_constraint_components.py @@ -355,18 +355,17 @@ def evaluate(self, target_graph, focus_value_nodes): if isinstance(v, bool) and v is True: # TODO:coverage: No test for when violation is `True` rept = self.make_v_result( - f, value_node=result_val, **rept_kwargs) + target_graph, f, value_node=result_val, **rept_kwargs) elif isinstance(v, tuple): t, p, v = v if v is None: v = result_val rept = self.make_v_result( - t or f, value_node=v, result_path=p, + target_graph, t or f, value_node=v, result_path=p, **rept_kwargs) else: rept = self.make_v_result( - f, value_node=v, - **rept_kwargs) + target_graph, f, value_node=v, **rept_kwargs) reports.append(rept) return (not non_conformant), reports diff --git a/pyshacl/constraints/sparql/sparql_based_constraints.py b/pyshacl/constraints/sparql/sparql_based_constraints.py index 06d2e95..422a36c 100644 --- a/pyshacl/constraints/sparql/sparql_based_constraints.py +++ b/pyshacl/constraints/sparql/sparql_based_constraints.py @@ -133,18 +133,17 @@ def _evaluate_sparql_constraint(self, sparql_constraint, non_conformant = True if isinstance(v, bool) and v is True: rept = self.make_v_result( - f, value_node=result_val, **rept_kwargs) + target_graph, f, value_node=result_val, **rept_kwargs) elif isinstance(v, tuple): t, p, v = v if v is None: v = result_val rept = self.make_v_result( - t or f, value_node=v, result_path=p, + target_graph, t or f, value_node=v, result_path=p, **rept_kwargs) else: rept = self.make_v_result( - f, value_node=v, - **rept_kwargs) + target_graph, f, value_node=v, **rept_kwargs) reports.append(rept) return non_conformant, reports diff --git a/pyshacl/rdfutil/clone.py b/pyshacl/rdfutil/clone.py index 777b1b0..727c513 100644 --- a/pyshacl/rdfutil/clone.py +++ b/pyshacl/rdfutil/clone.py @@ -11,7 +11,14 @@ def clone_dataset(source_ds, target_ds=None): default_union = source_ds.default_union if target_ds is None: target_ds = rdflib.Dataset(default_union=default_union) - cloned_graphs = [clone_graph(ng, rdflib.Graph(store=target_ds.store, identifier=ng.identifier)) for ng in source_ds.contexts()] + named_graphs = [ + rdflib.Graph(source_ds.store, i, namespace_manager=source_ds.namespace_manager) + if not isinstance(i, rdflib.Graph) else i for i in source_ds.store.contexts(None) + ] + cloned_graphs = [ + clone_graph(ng, rdflib.Graph(target_ds.store, ng.identifier, namespace_manager=target_ds.namespace_manager)) + for ng in named_graphs + ] default_context_id = target_ds.default_context.identifier for g in cloned_graphs: if g.identifier == default_context_id: diff --git a/pyshacl/rdfutil/stringify.py b/pyshacl/rdfutil/stringify.py index 87d65ce..257c81f 100644 --- a/pyshacl/rdfutil/stringify.py +++ b/pyshacl/rdfutil/stringify.py @@ -5,10 +5,12 @@ def stringify_blank_node(graph, bnode, ns_manager=None, recursion=0): + if isinstance(graph, (rdflib.ConjunctiveGraph, rdflib.Dataset)): + raise RuntimeError("Can only stringify a blank node when graph is an rdflib.Graph") assert isinstance(graph, rdflib.Graph) assert isinstance(bnode, rdflib.BNode) - if recursion >= 10: - return "Recursion too deep ..." + if recursion >= 9: + return "" stringed_cache_key = id(graph), str(bnode) if stringify_blank_node.stringed_cache is None: stringify_blank_node.stringed_cache = {} @@ -95,18 +97,33 @@ def stringify_literal(graph, node, ns_manager=None): return node_string +def find_node_named_graph(dataset, node): + if isinstance(node, rdflib.Literal): + raise RuntimeError("Cannot search for a Literal node in a dataset.") + for g in iter(dataset.contexts()): + try: + first = next(iter(g.predicate_objects(node))) + return g + except StopIteration: + continue + raise RuntimeError("Cannot find that node in any named graph.") + def stringify_node(graph, node, ns_manager=None, recursion=0): if ns_manager is None: ns_manager = graph.namespace_manager - ns_manager.bind("sh", SH) + if isinstance(ns_manager, rdflib.Graph): + #json-ld loader can set namespace_manager to the conjunctive graph itself. + ns_manager = ns_manager.namespace_manager + ns_manager.bind("sh", SH, override=False, replace=False) if isinstance(node, rdflib.Literal): - node_string = stringify_literal(graph, node, ns_manager=ns_manager) - elif isinstance(node, rdflib.BNode): - node_string = stringify_blank_node( - graph, node, ns_manager=ns_manager, - recursion=recursion+1) - elif isinstance(node, rdflib.URIRef): - node_string = node.n3(namespace_manager=ns_manager) + return stringify_literal(graph, node, ns_manager=ns_manager) + if isinstance(node, rdflib.BNode): + if isinstance(graph, (rdflib.ConjunctiveGraph, rdflib.Dataset)): + graph = find_node_named_graph(graph, node) + return stringify_blank_node(graph, node, ns_manager=ns_manager, + recursion=recursion+1) + if isinstance(node, rdflib.URIRef): + return node.n3(namespace_manager=ns_manager) else: node_string = str(node) return node_string diff --git a/pyshacl/validate.py b/pyshacl/validate.py index cfffd23..2521294 100644 --- a/pyshacl/validate.py +++ b/pyshacl/validate.py @@ -68,7 +68,10 @@ def _run_pre_inference(cls, target_graph, inference_option, logger=None): raise ReportableRuntimeError("Error during creation of OWL-RL Deductive Closure\n" "{}".format(str(e.args[0]))) if isinstance(target_graph, (rdflib.Dataset, rdflib.ConjunctiveGraph)): - named_graphs = [target_graph.get_context(i) if not isinstance(i, rdflib.Graph) else i for i in target_graph.store.contexts(None)] + named_graphs = [ + rdflib.Graph(target_graph.store, i, namespace_manager=target_graph.namespace_manager) + if not isinstance(i, rdflib.Graph) else i for i in target_graph.store.contexts(None) + ] else: named_graphs = [target_graph] try: @@ -104,9 +107,9 @@ def create_validation_report(cls, conforms, target_graph, shacl_graph, results): if isinstance(o, tuple): source = o[0] node = o[1] - if source == "S": + if source == 'S': o = clone_node(sg, node, vg) - elif source == "D": + elif source == 'D': o = clone_node(target_graph, node, vg) else: # pragma: no cover raise RuntimeError("Adding node to validation report must have source of either 'D' or 'S'.") @@ -145,7 +148,6 @@ def mix_in_ontology(self): return mix_graphs(self.data_graph, self.ont_graph) return mix_datasets(self.data_graph, self.ont_graph) - def run(self): if self.ont_graph is not None: # creates a copy of self.data_graph, doesn't modify it @@ -173,7 +175,10 @@ def run(self): else: advanced = {} if isinstance(the_target_graph, (rdflib.Dataset, rdflib.ConjunctiveGraph)): - named_graphs = [the_target_graph.get_context(i) if not isinstance(i, rdflib.Graph) else i for i in the_target_graph.store.contexts(None)] + named_graphs = [ + rdflib.Graph(the_target_graph.store, i, namespace_manager=the_target_graph.namespace_manager) + if not isinstance(i, rdflib.Graph) else i for i in the_target_graph.store.contexts(None) + ] else: named_graphs = [the_target_graph] for g in named_graphs: @@ -184,7 +189,8 @@ def run(self): _is_conform, _reports = s.validate(g) non_conformant = non_conformant or (not _is_conform) reports.extend(_reports) - v_report, v_text = self.create_validation_report((not non_conformant), the_target_graph, self.shacl_graph, reports) + v_report, v_text = self.create_validation_report( + not non_conformant, the_target_graph, self.shacl_graph, reports) return (not non_conformant), v_report, v_text diff --git a/test/issues/test_014.py b/test/issues/test_014.py index 15af97e..23bdedd 100644 --- a/test/issues/test_014.py +++ b/test/issues/test_014.py @@ -102,4 +102,3 @@ def test_014_pass(): if __name__ == "__main__": test_014_fail() test_014_pass() - test_014_web() \ No newline at end of file diff --git a/test/test_extra.py b/test/test_extra.py index e77b2e9..c20bdb2 100644 --- a/test/test_extra.py +++ b/test/test_extra.py @@ -5,6 +5,8 @@ # The need for these tests are discovered by doing coverage checks and these # are added as required. import os +import re + from pyshacl import validate from pyshacl.errors import ReportableRuntimeError @@ -118,7 +120,7 @@ ex:Pet1 rdf:type exOnt:Goanna ; rdf:label "Sebastian" ; - exOnt:nLegs "g"^^xsd:string . + exOnt:nLegs "four"^^xsd:string . """ def test_validate_with_ontology(): @@ -198,6 +200,54 @@ def test_metashacl_fail(): did_error = True assert did_error +data_file_text_bn = """ +@prefix rdf: . +@prefix xsd: . +@prefix exOnt: . +@prefix ex: . + +ex:Student1 exOnt:hasTeacher [ + rdf:type exOnt:PreschoolTeacher ; + rdf:label "Amy" ; + exOnt:nLegs "2"^^xsd:integer ; + exOnt:hasPet ex:Pet1 ] +. + +ex:Pet1 rdf:type exOnt:Goanna ; + rdf:label "Sebastian" ; + exOnt:nLegs "4"^^xsd:integer . +""" + +data_file_text_bad_bn = """ +@prefix rdf: . +@prefix xsd: . +@prefix exOnt: . +@prefix ex: . + +ex:Student1 exOnt:hasTeacher [ + rdf:type exOnt:PreschoolTeacher ; + rdf:label "Amy" ; + exOnt:nLegs "2"^^xsd:integer ; + exOnt:hasPet "Sebastian"^^xsd:string ] +. + +ex:Pet1 rdf:type exOnt:Goanna ; + rdf:label "Sebastian" ; + exOnt:nLegs "four"^^xsd:string . +""" + +def test_blank_node_string_generation(): + + res = validate(data_file_text_bad_bn, shacl_graph=shacl_file_text, + data_graph_format='turtle', shacl_graph_format='turtle', + ont_graph=ontology_file_text, ont_graph_format="turtle", + inference='rdfs', debug=True) + conforms, graph, string = res + assert not conforms + rx = r"^\s*Focus Node\:\s+\[.+rdf:type\s+.+exOnt\:PreschoolTeacher.*\]$" + matches = re.search(rx, string, flags=re.MULTILINE) + assert matches + def test_serialize_report_graph(): res = validate(data_file_text, shacl_graph=shacl_file_text, @@ -294,6 +344,7 @@ def test_owl_imports_fail(): test_validate_with_ontology_fail2() test_metashacl_pass() test_metashacl_fail() + test_blank_node_string_generation() test_web_retrieve() test_serialize_report_graph() test_owl_imports()