diff --git a/CHANGELOG.md b/CHANGELOG.md index 4060e49b..b7e30287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Python PEP 440 Versioning](https://www.python.org/d - CLI now accepts multiple data graph paths and adds a `--validate-each` flag. - New `validate_each()` entrypoint for per-graph validation in library code. +### Fixed +- SPARQL constraint validation no longer misidentifies `VALUES` in property paths or predicate names. + - Fixes #301 + ## [0.30.1] - 2025-03-15 ### Fixed diff --git a/pyshacl/cli.py b/pyshacl/cli.py index 7e1dd21b..d6a8fa6c 100644 --- a/pyshacl/cli.py +++ b/pyshacl/cli.py @@ -6,6 +6,7 @@ import sys from io import BufferedReader from typing import List, Union, cast + from prettytable import PrettyTable from pyshacl import __version__, validate, validate_each @@ -424,6 +425,7 @@ def _col_widther(s, w): i += w return '\n'.join(s2) + def write_validation_output(args, is_conform: bool, v_graph, v_text: str) -> None: if args.format == 'human': args.output.write(v_text) @@ -468,5 +470,6 @@ def write_validation_output(args, is_conform: bool, v_graph, v_text: str) -> Non v_graph = v_graph.decode('utf-8') args.output.write(v_graph) + if __name__ == "__main__": main() diff --git a/pyshacl/entrypoints.py b/pyshacl/entrypoints.py index d3dba2e3..30e4b435 100644 --- a/pyshacl/entrypoints.py +++ b/pyshacl/entrypoints.py @@ -19,7 +19,6 @@ from .validator import Validator, assign_baked_in from .validator_conformance import check_dash_result - DataGraphInput = Union[GraphLike, BufferedIOBase, TextIOBase, str, bytes] MultiDataGraphInput = Sequence[DataGraphInput] diff --git a/pyshacl/helper/sparql_query_helper.py b/pyshacl/helper/sparql_query_helper.py index d48124cf..84c89c8c 100644 --- a/pyshacl/helper/sparql_query_helper.py +++ b/pyshacl/helper/sparql_query_helper.py @@ -33,7 +33,11 @@ class SPARQLQueryHelper(object): bind_sg_regex = re.compile(r"([\s{}()])[\$\?]shapesGraph", flags=re.M) bind_cs_regex = re.compile(r"([\s{}()])[\$\?]currentShape", flags=re.M) has_minus_regex = re.compile(r"^(?:[^#]*|M)(?!#)#?[^\?\$\#]M?INUS[\s\{]", flags=re.M | re.I) - has_values_regex = re.compile(r"^(?:[^#]*|V)(?!#)#?[^\?\$\#]V?ALUES[\s\{]", flags=re.M | re.I) + # Match keyword VALUES without catching predicate names like ex:allowedValues + # Supports VALUES(?x), VALUES ?x, and VALUES { ... } + has_values_regex = re.compile( + r"^(?!\s*#).*?(? . +@prefix owl: . +@prefix xsd: . + +ex:myVariable + a ex:Variable, owl:NamedIndividual ; + ex:name "test_variable" ; + ex:datatype xsd:integer ; + ex:allowedValues ( "1"^^xsd:integer "test"^^xsd:string "5"^^xsd:integer) . +""" + +SHAPES_TTL = """\ +@prefix sh: . +@prefix ex: . +@prefix rdf: . + +ex:TestShape + a sh:NodeShape ; + sh:targetClass ex:Variable ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:message "Test constraint" ; + sh:select ''' + PREFIX ex: + PREFIX rdf: + SELECT $this WHERE { + $this ex:allowedValues ?list . + ?list rdf:rest/rdf:first ?val . + } + ''' ; + ] . +""" + + +def test_301(): + data_g = rdflib.Graph().parse(data=DATA_TTL, format="turtle") + shapes_g = rdflib.Graph().parse(data=SHAPES_TTL, format="turtle") + conforms, report_graph, results_text = pyshacl.validate( + data_g, + shacl_graph=shapes_g, + advanced=True, + debug=False, + ) + assert not conforms + assert not isinstance(report_graph, pyshacl.errors.ValidationFailure) + assert "Test constraint" in results_text + + +if __name__ == "__main__": + test_301() + sys.exit(test_301())