From 1eee61b58ffc3a33aaf4b213722ea55d70ca0e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sun, 30 Nov 2025 11:57:40 +0100 Subject: [PATCH 1/3] Add slice operator support in python --- CONTRIBUTING.md | 6 +- terminusdb_client/tests/test_slice.py | 76 ++++++++++++++++++ .../tests/woqljson/woqlSliceJson.py | 77 +++++++++++++++++++ terminusdb_client/woqlquery/woql_query.py | 48 ++++++++++++ 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 terminusdb_client/tests/test_slice.py create mode 100644 terminusdb_client/tests/woqljson/woqlSliceJson.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 745d3adf..3d27ae11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,11 @@ Thanks for interested to contribute to TerminusDB Client, to get started, fork t Make sure you have Python>=3.9 installed. We use [pipenv](https://pipenv-fork.readthedocs.io/en/latest/) for dev environment, to install pipenv: -`pip3 install pipenv --upgrade` +``` +python3 -m venv venv +source venv/bin/activate +pip3 install pipenv --upgrade +``` [Fork and clone](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repo, then in your local repo: diff --git a/terminusdb_client/tests/test_slice.py b/terminusdb_client/tests/test_slice.py new file mode 100644 index 00000000..4b03bc65 --- /dev/null +++ b/terminusdb_client/tests/test_slice.py @@ -0,0 +1,76 @@ +""" +Unit tests for WOQL slice operator + +Tests the Python client binding for slice(input_list, result, start, end=None) +""" + +import pytest +from terminusdb_client.woqlquery.woql_query import WOQLQuery + +from .woqljson.woqlSliceJson import WOQL_SLICE_JSON + + +class TestWOQLSlice: + """Test cases for the slice operator""" + + def test_basic_slice(self): + """AC-1: Basic slicing - slice([a,b,c,d], result, 1, 3) returns [b,c]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3) + assert woql_object.to_dict() == WOQL_SLICE_JSON["basicSlice"] + + def test_negative_indices(self): + """AC-3: Negative indices - slice([a,b,c,d], result, -2, -1) returns [c]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2, -1) + assert woql_object.to_dict() == WOQL_SLICE_JSON["negativeIndices"] + + def test_without_end(self): + """Optional end - slice([a,b,c,d], result, 1) returns [b,c,d]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1) + assert woql_object.to_dict() == WOQL_SLICE_JSON["withoutEnd"] + + def test_variable_list(self): + """Variable list input - slice(v:MyList, result, 0, 2)""" + woql_object = WOQLQuery().slice("v:MyList", "v:Result", 0, 2) + assert woql_object.to_dict() == WOQL_SLICE_JSON["variableList"] + + def test_variable_indices(self): + """Variable indices - slice([x,y,z], result, v:Start, v:End)""" + woql_object = WOQLQuery().slice(["x", "y", "z"], "v:Result", "v:Start", "v:End") + assert woql_object.to_dict() == WOQL_SLICE_JSON["variableIndices"] + + def test_empty_list(self): + """AC-6: Empty list - slice([], result, 0, 1) returns []""" + woql_object = WOQLQuery().slice([], "v:Result", 0, 1) + assert woql_object.to_dict() == WOQL_SLICE_JSON["emptyList"] + + def test_slice_type(self): + """Verify the @type is correctly set to 'Slice'""" + woql_object = WOQLQuery().slice(["a", "b"], "v:Result", 0, 1) + assert woql_object.to_dict()["@type"] == "Slice" + + def test_chaining_with_and(self): + """Test that slice works with method chaining via woql_and""" + woql_object = WOQLQuery().woql_and( + WOQLQuery().eq("v:MyList", ["a", "b", "c"]), + WOQLQuery().slice("v:MyList", "v:Result", 1, 3), + ) + result = woql_object.to_dict() + assert result["@type"] == "And" + assert len(result["and"]) == 2 + assert result["and"][1]["@type"] == "Slice" + + def test_single_element_slice(self): + """AC-2: Single element - slice([a,b,c,d], result, 1, 2) returns [b]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 2) + result = woql_object.to_dict() + assert result["@type"] == "Slice" + assert result["start"]["data"]["@value"] == 1 + assert result["end"]["data"]["@value"] == 2 + + def test_full_range(self): + """AC-7: Full range - slice([a,b,c,d], result, 0, 4) returns [a,b,c,d]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 0, 4) + result = woql_object.to_dict() + assert result["@type"] == "Slice" + assert result["start"]["data"]["@value"] == 0 + assert result["end"]["data"]["@value"] == 4 diff --git a/terminusdb_client/tests/woqljson/woqlSliceJson.py b/terminusdb_client/tests/woqljson/woqlSliceJson.py new file mode 100644 index 00000000..4183bb59 --- /dev/null +++ b/terminusdb_client/tests/woqljson/woqlSliceJson.py @@ -0,0 +1,77 @@ +"""Expected JSON output for WOQL slice operator tests""" + +WOQL_SLICE_JSON = { + "basicSlice": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 3}}, + }, + "negativeIndices": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": -2}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": -1}}, + }, + "withoutEnd": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}}, + # Note: no 'end' property when end is omitted + }, + "variableList": { + "@type": "Slice", + "list": {"@type": "DataValue", "variable": "MyList"}, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 0}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 2}}, + }, + "variableIndices": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "x"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "y"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "z"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "variable": "Start"}, + "end": {"@type": "DataValue", "variable": "End"}, + }, + "emptyList": { + "@type": "Slice", + "list": {"@type": "DataValue", "list": []}, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 0}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}}, + }, +} diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index 35fbad21..ee853af6 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2492,6 +2492,54 @@ def sum(self, user_input, output): self._cursor["sum"] = self._clean_data_value(output) return self + def slice(self, input_list, result, start, end=None): + """ + Extracts a contiguous subsequence from a list, following JavaScript's slice() semantics. + + Parameters + ---------- + input_list : list or str + A list of values or a variable representing a list + result : str + A variable that stores the sliced result + start : int or str + The start index (0-based, supports negative indices) + end : int or str, optional + The end index (exclusive). If omitted, takes the rest of the list + + Returns + ------- + WOQLQuery object + query object that can be chained and/or execute + + Examples + -------- + >>> WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3) # ["b", "c"] + >>> WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2) # ["c", "d"] + """ + if input_list and input_list == "args": + return ["list", "result", "start", "end"] + if self._cursor.get("@type"): + self._wrap_cursor_with_and() + self._cursor["@type"] = "Slice" + self._cursor["list"] = self._data_list(input_list) + self._cursor["result"] = self._clean_data_value(result) + if isinstance(start, int): + self._cursor["start"] = self._clean_data_value( + {"@type": "xsd:integer", "@value": start} + ) + else: + self._cursor["start"] = self._clean_data_value(start) + # end is optional + if end is not None: + if isinstance(end, int): + self._cursor["end"] = self._clean_data_value( + {"@type": "xsd:integer", "@value": end} + ) + else: + self._cursor["end"] = self._clean_data_value(end) + return self + def start(self, start, query=None): """Specifies that the start of the query returned From 4ccdd184f538d3d3ebce03e497266aea508881fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sun, 30 Nov 2025 12:07:40 +0100 Subject: [PATCH 2/3] lint fix --- terminusdb_client/tests/test_slice.py | 1 - terminusdb_client/woqlquery/woql_query.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/terminusdb_client/tests/test_slice.py b/terminusdb_client/tests/test_slice.py index 4b03bc65..74eec240 100644 --- a/terminusdb_client/tests/test_slice.py +++ b/terminusdb_client/tests/test_slice.py @@ -4,7 +4,6 @@ Tests the Python client binding for slice(input_list, result, start, end=None) """ -import pytest from terminusdb_client.woqlquery.woql_query import WOQLQuery from .woqljson.woqlSliceJson import WOQL_SLICE_JSON diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index ee853af6..2e930c82 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2494,7 +2494,7 @@ def sum(self, user_input, output): def slice(self, input_list, result, start, end=None): """ - Extracts a contiguous subsequence from a list, following JavaScript's slice() semantics. + Extracts a contiguous subsequence from a list, following slice() semantics. Parameters ---------- From 11c7785d6f235019201aa902cfb95fa8340adf46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sun, 30 Nov 2025 12:21:27 +0100 Subject: [PATCH 3/3] Add irrelevant identifiers --- terminusdb_client/tests/test_slice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/terminusdb_client/tests/test_slice.py b/terminusdb_client/tests/test_slice.py index 74eec240..38e76075 100644 --- a/terminusdb_client/tests/test_slice.py +++ b/terminusdb_client/tests/test_slice.py @@ -13,12 +13,12 @@ class TestWOQLSlice: """Test cases for the slice operator""" def test_basic_slice(self): - """AC-1: Basic slicing - slice([a,b,c,d], result, 1, 3) returns [b,c]""" + """Basic slicing - slice([a,b,c,d], result, 1, 3) returns [b,c]""" woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3) assert woql_object.to_dict() == WOQL_SLICE_JSON["basicSlice"] def test_negative_indices(self): - """AC-3: Negative indices - slice([a,b,c,d], result, -2, -1) returns [c]""" + """Negative indices - slice([a,b,c,d], result, -2, -1) returns [c]""" woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2, -1) assert woql_object.to_dict() == WOQL_SLICE_JSON["negativeIndices"] @@ -38,7 +38,7 @@ def test_variable_indices(self): assert woql_object.to_dict() == WOQL_SLICE_JSON["variableIndices"] def test_empty_list(self): - """AC-6: Empty list - slice([], result, 0, 1) returns []""" + """Empty list - slice([], result, 0, 1) returns []""" woql_object = WOQLQuery().slice([], "v:Result", 0, 1) assert woql_object.to_dict() == WOQL_SLICE_JSON["emptyList"] @@ -59,7 +59,7 @@ def test_chaining_with_and(self): assert result["and"][1]["@type"] == "Slice" def test_single_element_slice(self): - """AC-2: Single element - slice([a,b,c,d], result, 1, 2) returns [b]""" + """Single element - slice([a,b,c,d], result, 1, 2) returns [b]""" woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 2) result = woql_object.to_dict() assert result["@type"] == "Slice" @@ -67,7 +67,7 @@ def test_single_element_slice(self): assert result["end"]["data"]["@value"] == 2 def test_full_range(self): - """AC-7: Full range - slice([a,b,c,d], result, 0, 4) returns [a,b,c,d]""" + """Full range - slice([a,b,c,d], result, 0, 4) returns [a,b,c,d]""" woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 0, 4) result = woql_object.to_dict() assert result["@type"] == "Slice"