From c39e1946b16e7a0a9a677f1fa901ff3adfc2f4ac Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sun, 20 Apr 2025 09:26:48 +0200 Subject: [PATCH 1/3] chore(deps): install devtools as dev dependency --- pyproject.toml | 1 + uv.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 869411c..a223c36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ + "devtools>=0.12.2", "pytest>=8.3.4", "rdflib>=7.1.3", ] diff --git a/uv.lock b/uv.lock index dc0fd75..62379d0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,18 @@ version = 1 requires-python = ">=3.11" +[[package]] +name = "asttokens" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -10,6 +22,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "devtools" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/75/b78198620640d394bc435c17bb49db18419afdd6cfa3ed8bcfe14034ec80/devtools-0.12.2.tar.gz", hash = "sha256:efceab184cb35e3a11fa8e602cc4fadacaa2e859e920fc6f87bf130b69885507", size = 75005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/ae/afb1487556e2dc827a17097aac8158a25b433a345386f0e249f6d2694ccb/devtools-0.12.2-py3-none-any.whl", hash = "sha256:c366e3de1df4cdd635f1ad8cbcd3af01a384d7abda71900e68d43b04eb6aaca7", size = 19411 }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -38,6 +73,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "devtools" }, { name = "pytest" }, { name = "rdflib" }, ] @@ -47,6 +83,7 @@ requires-dist = [{ name = "lark", specifier = ">=1.2.2" }] [package.metadata.requires-dev] dev = [ + { name = "devtools", specifier = ">=0.12.2" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "rdflib", specifier = ">=7.1.3" }, ] @@ -69,6 +106,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + [[package]] name = "pyparsing" version = "3.2.1" @@ -104,3 +150,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/96/12/f43307e7b1f871ed5 wheels = [ { url = "https://files.pythonhosted.org/packages/9c/3c/f508a9b346078ea0bd49c8261430204fcfb4150352d51fa2a54a4d9eacda/rdflib-7.1.3-py3-none-any.whl", hash = "sha256:5402310a9f0f3c07d453d73fd0ad6ba35616286fe95d3670db2b725f3f539673", size = 564909 }, ] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] From 423649de52e3b18c49681daabe3609d60d486b48 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sun, 20 Apr 2025 09:32:55 +0200 Subject: [PATCH 2/3] test: use larql.SPARQLParser for tests --- tests/test_negative_parse_tests.py | 7 ++----- tests/test_positive_parse_tests.py | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_negative_parse_tests.py b/tests/test_negative_parse_tests.py index 7f0e8b8..b893631 100644 --- a/tests/test_negative_parse_tests.py +++ b/tests/test_negative_parse_tests.py @@ -1,9 +1,9 @@ from pathlib import Path -from lark import LarkError -from larql import SPARQLParser, sparql_parser import pytest +from lark import LarkError +from larql import SPARQLParser from tests.discovery import SPARQL11SyntaxTestDiscovery @@ -15,8 +15,5 @@ def test_positive_query_parsing(test_query_path): _query_file_path: Path = Path(test_query_path) query: str = _query_file_path.read_text() - with pytest.raises(LarkError): - sparql_parser.parse(query) - with pytest.raises(LarkError): SPARQLParser(query) diff --git a/tests/test_positive_parse_tests.py b/tests/test_positive_parse_tests.py index 99f2abb..a3a5cef 100644 --- a/tests/test_positive_parse_tests.py +++ b/tests/test_positive_parse_tests.py @@ -2,9 +2,9 @@ from pathlib import Path -from larql import SPARQLParser, sparql_parser import pytest +from larql import SPARQLParser from tests.discovery import SPARQL11SyntaxTestDiscovery @@ -16,5 +16,4 @@ def test_positive_query_parsing(test_query_path): _query_file_path: Path = Path(test_query_path) query = _query_file_path.read_text() - assert sparql_parser.parse(query) assert SPARQLParser(query) From 29526def57e0f8ecfd686596bdbbdced472f8ea8 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sun, 20 Apr 2025 09:33:41 +0200 Subject: [PATCH 3/3] feat: implement InsertDeleteQuadValidator --- src/larql/parser.py | 8 +++++-- src/larql/utils/validators.py | 42 +++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/larql/parser.py b/src/larql/parser.py index 5a45921..a107e22 100644 --- a/src/larql/parser.py +++ b/src/larql/parser.py @@ -4,7 +4,7 @@ from pathlib import Path import lark -from larql.utils.validators import UnicodeValidator +from larql.utils.validators import InsertDeleteQuadValidator, UnicodeValidator _package_path = Path(files("larql")) # type: ignore @@ -19,7 +19,11 @@ def __init__(self, query: str) -> None: self.query = query self.tree: lark.Tree = sparql_parser.parse(self.query) - self._run_validators(UnicodeValidator()) + self._run_validators( + # UnicodeValidator might be useless; UTF-8 should already be checked in the grammar now + UnicodeValidator(), + InsertDeleteQuadValidator(), + ) def _run_validators(self, *validators: lark.Visitor): """Run validators against the Tree component.""" diff --git a/src/larql/utils/validators.py b/src/larql/utils/validators.py index 58104ec..dd3945c 100644 --- a/src/larql/utils/validators.py +++ b/src/larql/utils/validators.py @@ -3,6 +3,10 @@ import lark +class BindScopeValidator(lark.Visitor): + """Validator for checking that bound variables are not previously used.""" + + class UnicodeValidator(lark.Visitor): """Validator for checking that string terminals are UTF-8 encodable.""" @@ -18,5 +22,39 @@ def string(self, node): raise lark.ParseError(message) from e -class GroupByValidator(lark.Visitor): - pass +class InsertDeleteQuadValidator(lark.Visitor): + """Validator for checking semantic quad constraints in INSERT/DELETE environments.""" + + def _validate_children( + self, node: lark.Tree, disallowed: set[str], error_message + ) -> None: + for child in node.children: + if isinstance(child, lark.Token): + continue + else: + if child.data in disallowed: + message = error_message.format(child=child) + raise lark.ParseError(message) + + self._validate_children(child, disallowed, error_message) + + def insert_data(self, node: lark.Tree) -> None: + message = "Variable nodes are not allowed in INSERT DATA. Found node '{child}'." + self._validate_children(node, {"var"}, message) + + def delete_data(self, node: lark.Tree) -> None: + message = "Variables and blank nodes are not allowed in DELETE DATA contexts. Found node: '{child}'." + self._validate_children(node, {"var", "blank_node"}, message) + + def delete_clause(self, node: lark.Tree) -> None: + message = ( + "Blank nodes are not allowed in DELETE clauses.\nFound node: '{child}'." + ) + self._validate_children(node, {"blank_node"}, message) + + def delete_where(self, node: lark.Tree) -> None: + message = ( + "Blank nodes are not allowed in DELETE WHERE contexts.\n" + "Found node: '{child}'." + ) + self._validate_children(node, {"blank_node"}, message)