You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2021/01/27 01:41:10 UTC

[tinkerpop] 01/01: TINKERPOP-2395 Support list as keys in Map for python

This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch TINKERPOP-2395
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 761f4dc30aef596d4b9dc060af57ea231c82fa0b
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Tue Jan 26 20:40:26 2021 -0500

    TINKERPOP-2395 Support list as keys in Map for python
---
 CHANGELOG.asciidoc                                  |  2 +-
 docs/src/upgrade/release-3.5.x.asciidoc             |  9 +++++----
 .../Gherkin/GherkinTestRunner.cs                    |  4 ++--
 .../Gherkin/IgnoreException.cs                      |  7 ++++++-
 .../gremlin-javascript/test/cucumber/gremlin.js     |  1 +
 .../main/python/gremlin_python/structure/io/util.py |  5 ++++-
 .../src/main/python/radish/feature_steps.py         |  4 +++-
 gremlin-python/src/main/python/radish/gremlin.py    |  1 +
 .../tests/driver/test_driver_remote_connection.py   |  3 +++
 gremlin-test/features/sideEffect/GroupCount.feature | 13 ++++++++++++-
 .../traversal/step/sideEffect/GroupCountTest.java   | 21 +++++++++++++++++++++
 11 files changed, 59 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index ef9d2dc..486765b 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -32,7 +32,7 @@ This release also includes changes from <<release-3-4-3, 3.4.3>>.
 * Allowed `property(T.label,Object)` to be used if no value was supplied to `addV(String)`.
 * Allowed additional arguments to `Client.submit()` in Javascript driver to enable setting of parameters like `scriptEvaluationTimeout`.
 * Gremlin.Net driver no longer supports skipping deserialization by default. Users can however create their own `IMessageSerializer` if they need this functionality.
-* Supported deserialization of `dict` as a key in a `dict` for Python.
+* Supported deserialization of `dict` and `list` as a key in a `dict` for Python.
 * Added a `Graph.Feature` for `supportsNullPropertyValues`.
 * Modified `TokenTraversal` to support `Property` thus `by(key)` and `by(value)` can now apply to `Edge` and meta-properties.
 * Added `SeedStrategy` to allow deterministic behavior for `coin()`, `sample()` and `Order.shuffle`.
diff --git a/docs/src/upgrade/release-3.5.x.asciidoc b/docs/src/upgrade/release-3.5.x.asciidoc
index 01296aa..8e5618d 100644
--- a/docs/src/upgrade/release-3.5.x.asciidoc
+++ b/docs/src/upgrade/release-3.5.x.asciidoc
@@ -354,9 +354,9 @@ See: link:https://issues.apache.org/jira/browse/TINKERPOP-1682[TINKERPOP-1682]
 
 ==== Complex dict Deserialization
 
-In Gremlin it is common to return a `dict` as a key value in another `dict`. The problem for Python is that a `dict`
-is not hashable and will result in an error. By introducing a `HashableDict` for those keys, it is now possible to
-return these types of results and not have to work around them:
+In Gremlin it is common to return a `dict` or `list` as a key value in another `dict`. The problem for Python is that
+these values are not hashable and will result in an error. By introducing a `HashableDict` and `Tuple` for those keys
+(respectively), it is now possible to return these types of results and not have to work around them:
 
 [source,text]
 ----
@@ -364,7 +364,8 @@ return these types of results and not have to work around them:
 {{<T.id: 1>: 1, <T.label: 4>: 'person', 'name': 'marko'}: 1}
 ----
 
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-2395[TINKERPOP-2395]
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2395[TINKERPOP-2395],
+link:https://issues.apache.org/jira/browse/TINKERPOP-2407[TINKERPOP-2407]
 
 ==== Gremlin Server Audit Logging
 
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs
index 2649e68..e596448 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs
@@ -41,8 +41,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
             new Dictionary<string, IgnoreReason>
             {
                 // Add here the name of scenarios to ignore and the reason, e.g.:
-                { "g_V_group_byXageX", IgnoreReason.NullKeysInMapNotSupported }
-                //{ "g_V_properties_propertiesXstartTimeX_drop", IgnoreReason.NoReason },
+                { "g_V_group_byXageX", IgnoreReason.NullKeysInMapNotSupported },
+                { "g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX", IgnoreReason.ArrayKeysInMapNotAssertingInGherkin }
             };
         
         private static class Keywords
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs
index 0e25440..9579626 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs
@@ -55,6 +55,11 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
         /// <summary>
         /// C# does not allow a `null` value to be used as a key.
         /// </summary>
-        NullKeysInMapNotSupported
+        NullKeysInMapNotSupported,
+        
+        /// <summary>
+        /// C# array equality is by reference not contents so the gherkin setup won't assert properly
+        /// </summary>
+        ArrayKeysInMapNotAssertingInGherkin
     }
 }
\ No newline at end of file
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
index 43545bd..d393d47 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
@@ -651,6 +651,7 @@ const gremlins = {
     g_V_outXcreatedX_groupCountXxX_capXxX: [function({g}) { return g.V().out("created").groupCount("x").cap("x") }], 
     g_V_groupCount_byXbothE_countX: [function({g}) { return g.V().groupCount().by(__.bothE().count()) }], 
     g_V_both_groupCountXaX_out_capXaX_selectXkeysX_unfold_both_groupCountXaX_capXaX: [function({g}) { return g.V().both().groupCount("a").out().cap("a").select(Column.keys).unfold().both().groupCount("a").cap("a") }], 
+    g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX: [function({g}) { return g.V().has("person","name","marko").both("knows").groupCount().by(__.values("name").fold()) }], 
     g_VX1X_out_injectXv2X_name: [function({g, v1Id, v2}) { return g.V(v1Id).out().inject(v2).values("name") }], 
     g_VX1X_out_name_injectXdanielX_asXaX_mapXlengthX_path: [function({g, v1Id, c}) { return g.V(v1Id).out().values("name").inject("daniel").as("a").map(c).path() }], 
     g_VX1X_injectXg_VX4XX_out_name: [function({g, v1Id, v4}) { return g.V(v1Id).inject(v4).out().values("name") }], 
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/io/util.py b/gremlin-python/src/main/python/gremlin_python/structure/io/util.py
index 86fd72a..e57a7e9 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/io/util.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/io/util.py
@@ -32,6 +32,9 @@ class HashableDict(dict):
 
         new_o = HashableDict()
         for k, v in o.items():
-            new_o[k] = cls.of(v)
+            if isinstance(k, (set, list)):
+                new_o[tuple(k)] = cls.of(v)
+            else:
+                new_o[k] = cls.of(v)
         return new_o
 
diff --git a/gremlin-python/src/main/python/radish/feature_steps.py b/gremlin-python/src/main/python/radish/feature_steps.py
index 826cc75..fe1ea44 100644
--- a/gremlin-python/src/main/python/radish/feature_steps.py
+++ b/gremlin-python/src/main/python/radish/feature_steps.py
@@ -143,7 +143,9 @@ def _convert(val, ctx):
     if isinstance(val, dict):                                            # convert dictionary keys/values
         n = {}
         for key, value in val.items():
-            n[_convert(key, ctx)] = _convert(value, ctx)
+            k = _convert(key, ctx)
+            # convert to tuple key if list/set as neither are hashable
+            n[tuple(k) if isinstance(k, (set, list)) else k] = _convert(value, ctx)
         return n
     elif isinstance(val, str) and re.match(r"^l\[.*\]$", val):           # parse list
         return [] if val == "l[]" else list(map((lambda x: _convert(x, ctx)), val[2:-1].split(",")))
diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py
index 5e16f57..45a10bb 100644
--- a/gremlin-python/src/main/python/radish/gremlin.py
+++ b/gremlin-python/src/main/python/radish/gremlin.py
@@ -636,6 +636,7 @@ world.gremlins = {
     'g_V_outXcreatedX_groupCountXxX_capXxX': [(lambda g:g.V().out('created').groupCount('x').cap('x'))], 
     'g_V_groupCount_byXbothE_countX': [(lambda g:g.V().groupCount().by(__.bothE().count()))], 
     'g_V_both_groupCountXaX_out_capXaX_selectXkeysX_unfold_both_groupCountXaX_capXaX': [(lambda g:g.V().both().groupCount('a').out().cap('a').select(Column.keys).unfold().both().groupCount('a').cap('a'))], 
+    'g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX': [(lambda g:g.V().has('person','name','marko').both('knows').groupCount().by(__.name.fold()))], 
     'g_VX1X_out_injectXv2X_name': [(lambda g, v1Id=None,v2=None:g.V(v1Id).out().inject(v2).name)], 
     'g_VX1X_out_name_injectXdanielX_asXaX_mapXlengthX_path': [(lambda g, v1Id=None,c=None:g.V(v1Id).out().name.inject('daniel').as_('a').map(c).path())], 
     'g_VX1X_injectXg_VX4XX_out_name': [(lambda g, v1Id=None,v4=None:g.V(v1Id).inject(v4).out().name)], 
diff --git a/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py b/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py
index bb7793f..4c95509 100644
--- a/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py
+++ b/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py
@@ -104,6 +104,9 @@ class TestDriverRemoteConnection(object):
         if not isinstance(remote_connection._client._message_serializer, GraphSONSerializersV2d0):
             results = g.V().has('person', 'name', 'marko').elementMap("name").groupCount().next()
             assert {HashableDict.of({T.id: 1, T.label: 'person', 'name': 'marko'}): 1} == results
+        if not isinstance(remote_connection._client._message_serializer, GraphSONSerializersV2d0):
+            results = g.V().has('person', 'name', 'marko').both('knows').groupCount().by(__.values('name').fold()).next()
+            assert {tuple(['vadas']): 1, tuple(['josh']): 1} == results
 
     def test_lambda_traversals(self, remote_connection):
         statics.load_statics(globals())
diff --git a/gremlin-test/features/sideEffect/GroupCount.feature b/gremlin-test/features/sideEffect/GroupCount.feature
index 40978cb..53acd6d 100644
--- a/gremlin-test/features/sideEffect/GroupCount.feature
+++ b/gremlin-test/features/sideEffect/GroupCount.feature
@@ -190,4 +190,15 @@ Feature: Step - groupCount()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"v[marko]":"d[6].l","v[vadas]":"d[2].l","v[lop]":"d[6].l","v[josh]":"d[6].l","v[ripple]":"d[2].l","v[peter]":"d[2].l"}] |
\ No newline at end of file
+      | m[{"v[marko]":"d[6].l","v[vadas]":"d[2].l","v[lop]":"d[6].l","v[josh]":"d[6].l","v[ripple]":"d[2].l","v[peter]":"d[2].l"}] |
+
+  Scenario: g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().has("person", "name", "marko").both("knows").groupCount().by(__.values("name").fold())
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | m[{"l[josh]":"d[1].l","l[vadas]":"d[1].l"}] |
\ No newline at end of file
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/GroupCountTest.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/GroupCountTest.java
index 34011ac..1051e2a 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/GroupCountTest.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/GroupCountTest.java
@@ -30,7 +30,9 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData.MODERN;
@@ -76,6 +78,8 @@ public abstract class GroupCountTest extends AbstractGremlinProcessTest {
 
     public abstract Traversal<Vertex, String> get_g_V_both_groupCountXaX_byXlabelX_asXbX_barrier_whereXselectXaX_selectXsoftwareX_isXgtX2XXX_selectXbX_name();
 
+    public abstract Traversal<Vertex, Map<Object, Long>> get_g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX();
+
     @Test
     @LoadGraphWith(MODERN)
     public void g_V_outXcreatedX_groupCount_byXnameX() {
@@ -258,6 +262,18 @@ public abstract class GroupCountTest extends AbstractGremlinProcessTest {
         checkSideEffects(traversal.asAdmin().getSideEffects(), "a", HashMap.class);
     }
 
+    @Test
+    @LoadGraphWith(MODERN)
+    public void g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX() {
+        final Traversal<Vertex, Map<Object,Long>> traversal = get_g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX();
+        printTraversalForm(traversal);
+        final Map<Object,Long> map = traversal.next();
+        assertFalse(traversal.hasNext());
+        assertEquals(2, map.size());
+        assertEquals(1L, map.get(Collections.singletonList("josh")).longValue());
+        assertEquals(1L, map.get(Collections.singletonList("vadas")).longValue());
+    }
+
     public static class Traversals extends GroupCountTest {
 
         @Override
@@ -326,5 +342,10 @@ public abstract class GroupCountTest extends AbstractGremlinProcessTest {
         public Traversal<Vertex, String> get_g_V_both_groupCountXaX_byXlabelX_asXbX_barrier_whereXselectXaX_selectXsoftwareX_isXgtX2XXX_selectXbX_name() {
             return g.V().both().groupCount("a").by(T.label).as("b").barrier().where(__.select("a").select("software").is(gt(2))).select("b").values("name");
         }
+
+        @Override
+        public Traversal<Vertex, Map<Object, Long>> get_g_V_hasXperson_name_markoX_bothXknowsX_groupCount_byXvaluesXnameX_foldX() {
+            return g.V().has("person", "name", "marko").both("knows").groupCount().by(__.values("name").fold());
+        }
     }
 }
\ No newline at end of file