You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by xi...@apache.org on 2026/03/25 23:07:59 UTC
(tinkerpop) 01/02: Use HashableDict for non-hashable elements in set
This is an automated email from the ASF dual-hosted git repository.
xiazcy pushed a commit to branch TINKERPOP-3232
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 0d212d44c19aa063694632a6ca562b794abb2bb2
Author: Yang Xia <xi...@gmail.com>
AuthorDate: Wed Mar 25 15:18:41 2026 -0700
Use HashableDict for non-hashable elements in set
---
.../gremlin_python/structure/io/graphbinaryV1.py | 6 ++-
.../gremlin_python/structure/io/graphsonV3d0.py | 9 +++-
.../driver/test_driver_remote_connection.py | 15 ++++++
.../python/tests/unit/io/test_graphbinaryV1.py | 60 ++++++++++++++++++++++
.../main/python/tests/unit/io/test_graphsonV3d0.py | 58 +++++++++++++++++++++
5 files changed, 146 insertions(+), 2 deletions(-)
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py
index 33ec78431b..b9b3338d82 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py
@@ -520,7 +520,11 @@ class SetDeserializer(ListIO):
@classmethod
def objectify(cls, buff, reader, nullable=True):
- return set(ListIO.objectify(buff, reader, nullable))
+ the_list = ListIO.objectify(buff, reader, nullable)
+ try:
+ return set(the_list)
+ except TypeError:
+ return set(HashableDict.of(e) for e in the_list)
class MapIO(_GraphBinaryTypeIO):
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV3d0.py b/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV3d0.py
index 03415c69e2..4d25816abd 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV3d0.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV3d0.py
@@ -438,9 +438,16 @@ class SetIO(_GraphSONTypeIO):
In case Java returns numeric values of different types which
python don't recognize, coerce and return a list.
See comments of TINKERPOP-1844 for more details
+
+ In case the set contains non-hashable elements (e.g. dict, list),
+ use HashableDict.of() to make them hashable.
+ See TINKERPOP-3232 for more details
"""
new_list = [reader.to_object(obj) for obj in s]
- new_set = set(new_list)
+ try:
+ new_set = set(new_list)
+ except TypeError:
+ new_set = set(HashableDict.of(e) for e in new_list)
if len(new_list) != len(new_set):
log.warning("Coercing g:Set to list due to java numeric values. "
"See TINKERPOP-1844 for more details.")
diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py b/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py
index e59ec942ec..72f45b8aa9 100644
--- a/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py
+++ b/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py
@@ -26,6 +26,7 @@ from gremlin_python.process.traversal import Traverser
from gremlin_python.process.traversal import TraversalStrategy
from gremlin_python.process.traversal import Bindings
from gremlin_python.process.traversal import P, Order, T
+from gremlin_python.process.traversal import Scope
from gremlin_python.process.graph_traversal import __
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.structure.graph import Vertex
@@ -143,6 +144,20 @@ class TestDriverRemoteConnection(object):
assert p.objects[1].properties is not None and len(p.objects[1].properties) > 0
assert p.objects[2].properties is not None and len(p.objects[2].properties) > 0
+ def test_set_with_unhashable_elements(self, remote_connection):
+ # test that a query returning a Set containing non-hashable elements (maps) can be
+ # deserialized without a TypeError - see TINKERPOP-3232
+ # GraphSON v2 does not have a Set type so it deserializes as list - skip for v2
+ if isinstance(remote_connection._client._message_serializer, GraphSONSerializersV2d0):
+ return
+ g = traversal().withRemote(remote_connection)
+ # g.V().valueMap().dedup(Scope.local) returns a Set of Map results which previously
+ # threw TypeError because Python sets cannot contain unhashable dict elements
+ results = g.V().valueMap().dedup(Scope.local).toList()
+ assert len(results) > 0
+ for r in results:
+ assert isinstance(r, set)
+
def test_lambda_traversals(self, remote_connection):
statics.load_statics(globals())
assert "remoteconnection[{},gmodern]".format(test_no_auth_url) == str(remote_connection)
diff --git a/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py b/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py
index 92c8edc4ba..30105c6659 100644
--- a/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py
+++ b/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py
@@ -24,6 +24,7 @@ import math
from gremlin_python.statics import timestamp, long, bigint, BigDecimal, SingleByte, SingleChar, ByteBufferType
from gremlin_python.structure.graph import Vertex, Edge, Property, VertexProperty, Path
from gremlin_python.structure.io.graphbinaryV1 import GraphBinaryWriter, GraphBinaryReader
+from gremlin_python.structure.io.util import HashableDict
from gremlin_python.process.traversal import Barrier, Binding, Bytecode, Merge, Direction
@@ -122,6 +123,65 @@ class TestGraphSONWriter(object):
output = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
assert x == output
+ def test_set_with_unhashable_dict_elements(self):
+ # test that sets containing dicts can be deserialized - see TINKERPOP-3232
+ x = [{"name": "marko", "age": 29}, {"name": "josh", "age": 32}]
+ output = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(set(
+ HashableDict.of(e) for e in x)))
+ assert isinstance(output, set)
+ assert len(output) == 2
+
+ def test_set_with_unhashable_list_elements(self):
+ # test that sets containing lists can be deserialized - see TINKERPOP-3232
+ # build a set payload manually: write as list-of-lists, then patch the type byte to set
+ list_payload = self.graphbinary_writer.write_object([["marko", "josh"], ["vadas", "peter"]])
+ # the first byte is the DataType for list (0x09), change it to set (0x0b)
+ set_payload = bytearray(list_payload)
+ set_payload[0] = 0x0b
+ output = self.graphbinary_reader.read_object(set_payload)
+ assert isinstance(output, set)
+ assert len(output) == 2
+
+ def test_set_with_unhashable_set_elements(self):
+ # test that sets containing sets can be deserialized - see TINKERPOP-3232
+ # build a set-of-sets payload: write as list-of-lists, patch outer and inner type bytes
+ inner1 = self.graphbinary_writer.write_object(["a", "b"])
+ inner2 = self.graphbinary_writer.write_object(["c", "d"])
+ # patch inner payloads from list (0x09) to set (0x0b)
+ inner1 = bytearray(inner1)
+ inner1[0] = 0x0b
+ inner2 = bytearray(inner2)
+ inner2[0] = 0x0b
+ # build outer set: type byte (set=0x0b) + nullable (0x00) + count (2) + inner payloads
+ import struct
+ outer = bytearray([0x0b, 0x00]) + struct.pack('>i', 2) + inner1 + inner2
+ output = self.graphbinary_reader.read_object(outer)
+ assert isinstance(output, set)
+ assert len(output) == 2
+
+ def test_set_with_mixed_hashable_and_unhashable_elements(self):
+ # test that sets containing a mix of hashable and unhashable elements work - see TINKERPOP-3232
+ # build payload: write a list of [string, dict, int], then patch type to set
+ x = ["marko", {"name": "josh"}, 42]
+ list_payload = self.graphbinary_writer.write_object(x)
+ set_payload = bytearray(list_payload)
+ set_payload[0] = 0x0b
+ output = self.graphbinary_reader.read_object(set_payload)
+ assert isinstance(output, set)
+ assert len(output) == 3
+
+ def test_set_with_nested_unhashable_elements(self):
+ # test that sets containing dicts with list values can be deserialized - see TINKERPOP-3232
+ # build payload manually since HashableDict.of() converts lists to tuples which can't be serialized
+ x = [{"name": "marko", "langs": ["java", "python"]}, {"name": "josh", "langs": ["gremlin"]}]
+ list_payload = self.graphbinary_writer.write_object(x)
+ # patch outer type from list (0x09) to set (0x0b)
+ set_payload = bytearray(list_payload)
+ set_payload[0] = 0x0b
+ output = self.graphbinary_reader.read_object(set_payload)
+ assert isinstance(output, set)
+ assert len(output) == 2
+
def test_dict(self):
x = {"yo": "what?",
"go": "no!",
diff --git a/gremlin-python/src/main/python/tests/unit/io/test_graphsonV3d0.py b/gremlin-python/src/main/python/tests/unit/io/test_graphsonV3d0.py
index fd98da313d..027fb3a4bd 100644
--- a/gremlin-python/src/main/python/tests/unit/io/test_graphsonV3d0.py
+++ b/gremlin-python/src/main/python/tests/unit/io/test_graphsonV3d0.py
@@ -32,6 +32,7 @@ from gremlin_python.statics import *
from gremlin_python.structure.graph import Vertex, Edge, Property, VertexProperty, Path
from gremlin_python.structure.io.graphsonV3d0 import GraphSONWriter, GraphSONReader, GraphSONUtil
import gremlin_python.structure.io.graphsonV3d0
+from gremlin_python.structure.io.util import HashableDict
from gremlin_python.process.traversal import P, Merge, Barrier, Order, Operator, Direction
from gremlin_python.process.strategies import SubgraphStrategy
from gremlin_python.process.graph_traversal import __
@@ -86,6 +87,63 @@ class TestGraphSONReader:
assert x.count("marko") == 1
assert x.count("josh") == 3
+ def test_set_with_unhashable_dict_elements(self):
+ # test that sets containing dicts can be deserialized - see TINKERPOP-3232
+ x = self.graphson_reader.read_object(
+ json.dumps({"@type": "g:Set", "@value": [
+ {"@type": "g:Map", "@value": ["name", "marko", "age", {"@type": "g:Int32", "@value": 29}]},
+ {"@type": "g:Map", "@value": ["name", "josh", "age", {"@type": "g:Int32", "@value": 32}]}
+ ]}))
+ assert isinstance(x, set)
+ assert len(x) == 2
+
+ def test_set_with_unhashable_list_elements(self):
+ # test that sets containing lists can be deserialized - see TINKERPOP-3232
+ x = self.graphson_reader.read_object(
+ json.dumps({"@type": "g:Set", "@value": [
+ {"@type": "g:List", "@value": ["marko", "josh"]},
+ {"@type": "g:List", "@value": ["vadas", "peter"]}
+ ]}))
+ assert isinstance(x, set)
+ assert len(x) == 2
+
+ def test_set_with_unhashable_set_elements(self):
+ # test that sets containing sets can be deserialized - see TINKERPOP-3232
+ x = self.graphson_reader.read_object(
+ json.dumps({"@type": "g:Set", "@value": [
+ {"@type": "g:Set", "@value": ["a", "b"]},
+ {"@type": "g:Set", "@value": ["c", "d"]}
+ ]}))
+ assert isinstance(x, set)
+ assert len(x) == 2
+
+ def test_set_with_mixed_hashable_and_unhashable_elements(self):
+ # test that sets containing a mix of hashable and unhashable elements work - see TINKERPOP-3232
+ x = self.graphson_reader.read_object(
+ json.dumps({"@type": "g:Set", "@value": [
+ "marko",
+ {"@type": "g:Map", "@value": ["name", "josh"]},
+ {"@type": "g:Int32", "@value": 42}
+ ]}))
+ assert isinstance(x, set)
+ assert len(x) == 3
+
+ def test_set_with_nested_unhashable_elements(self):
+ # test that sets containing dicts with list values can be deserialized - see TINKERPOP-3232
+ x = self.graphson_reader.read_object(
+ json.dumps({"@type": "g:Set", "@value": [
+ {"@type": "g:Map", "@value": [
+ "name", "marko",
+ "langs", {"@type": "g:List", "@value": ["java", "python"]}
+ ]},
+ {"@type": "g:Map", "@value": [
+ "name", "josh",
+ "langs", {"@type": "g:List", "@value": ["gremlin"]}
+ ]}
+ ]}))
+ assert isinstance(x, set)
+ assert len(x) == 2
+
def test_number_input(self):
x = self.graphson_reader.read_object(json.dumps({
"@type": "gx:Byte",