Skip to content

Commit fa48185

Browse files
committed
TINKERPOP-2063 Added subgraph support to python
1 parent 4ebe155 commit fa48185

9 files changed

Lines changed: 236 additions & 39 deletions

File tree

CHANGELOG.asciidoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
2323
[[release-4-0-0]]
2424
=== TinkerPop 4.0.0 (NOT OFFICIALLY RELEASED YET)
2525
26+
* Added `subgraph()` support for `gremlin-python` so that results are stored in a detached `Graph` object.
2627
* Modified grammar to make `discard()` usage more consistent as a filter step where it can now be used to chain additional traversal steps and be used anonymously.
2728
* Removed `Meta` field from `ResponseResult` struct in `gremlin-go`
2829
* Removed deprecated elements of the Java-based process testing suite: `ProcessStandardSuite`, `ProcessComputerSuite`, `ProcessLimitedSuite` and associated tests.
@@ -180,7 +181,7 @@ This release also includes changes from <<release-3-7-5, 3.7.5>>.
180181
* Fixed `BigInt` and `BigDecimal` parsing in `gremlin-go` cucumber test suite, fixed `UnscaledValue` type in `BigDecimal` struct and added `ParseBigDecimal` method.
181182
* Added boolean parsing step `asBool()`.
182183
* Added validation to `valueMap()`, `propertyMap()`, `groupCount()`, `sack()`, `dedup()`, `sample()`, and `aggregate()` to prevent the invalid usage of multiple `by()` modulators.
183-
* Deprecated `ProcessEmbeddedStandardSuite` and `ProcessEmbeddedComputerSuite` in favor of `ProcessEmbeddedStandardSuite` and `ProcessEmbeddedComputerSuite` respectively.
184+
* Deprecated `ProcessLimitedStandardSuite` and `ProcessLimitedComputerSuite` in favor of `ProcessEmbeddedStandardSuite` and `ProcessEmbeddedComputerSuite` respectively.
184185
* Deprecated `ProcessStandardSuite` and the `ProcessComputerSuite` in favor of Gherkin testing and the `ProcessEmbeddedStandardSuite` and `ProcessEmbeddedComputerSuite` for testing JVM-specific Gremlin behaviors.
185186
* Removed lambda oriented Gremlin testing from Gherkin test suite.
186187
* Implemented `P.typeOf()` predicate.

gremlin-python/src/main/python/gremlin_python/structure/graph.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222

2323
class Graph(object):
24+
def __init__(self):
25+
self.vertices = {}
26+
self.edges = {}
2427

2528
def __repr__(self):
2629
return "graph[]"
@@ -32,6 +35,18 @@ def __init__(self, id, label, properties=None):
3235
self.label = label
3336
self.properties = [] if properties is None else properties
3437

38+
def __getitem__(self, key):
39+
for p in self.properties:
40+
if p.key == key:
41+
return p.value
42+
raise KeyError(key)
43+
44+
def values(self, *property_keys):
45+
if len(property_keys) == 0:
46+
return [p.value for p in self.properties]
47+
else:
48+
return [p.value for p in self.properties if p.key in property_keys]
49+
3550
def __eq__(self, other):
3651
return isinstance(other, self.__class__) and self.id == other.id
3752

gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,11 +639,82 @@ class TinkerGraphIO(_GraphBinaryTypeIO):
639639

640640
@classmethod
641641
def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True):
642-
raise AttributeError("TinkerGraph serialization is not currently supported by gremlin-python")
642+
cls.prefix_bytes(cls.graphbinary_type, as_value, nullable, to_extend)
643+
644+
vertices = list(obj.vertices.values())
645+
edges = list(obj.edges.values())
646+
647+
IntIO.dictify(len(vertices), writer, to_extend, True, False)
648+
for v in vertices:
649+
writer.to_dict(v.id, to_extend)
650+
ListIO.dictify([v.label], writer, to_extend, True, False)
651+
v_props = v.properties
652+
IntIO.dictify(len(v_props), writer, to_extend, True, False)
653+
for vp in v_props:
654+
writer.to_dict(vp.id, to_extend)
655+
ListIO.dictify([vp.label], writer, to_extend, True, False)
656+
writer.to_dict(vp.value, to_extend)
657+
writer.to_dict(None, to_extend)
658+
ListIO.dictify(vp.properties, writer, to_extend, True, False)
659+
660+
IntIO.dictify(len(edges), writer, to_extend, True, False)
661+
for e in edges:
662+
writer.to_dict(e.id, to_extend)
663+
ListIO.dictify([e.label], writer, to_extend, True, False)
664+
writer.to_dict(e.inV.id, to_extend)
665+
writer.to_dict(None, to_extend)
666+
writer.to_dict(e.outV.id, to_extend)
667+
writer.to_dict(None, to_extend)
668+
writer.to_dict(None, to_extend)
669+
ListIO.dictify(e.properties, writer, to_extend, True, False)
670+
671+
return to_extend
643672

644673
@classmethod
645-
def objectify(cls, b, reader, as_value=False):
646-
raise AttributeError("TinkerGraph deserialization is not currently supported by gremlin-python")
674+
def objectify(cls, buff, reader, nullable=True):
675+
return cls.is_null(buff, reader, cls._read_graph, nullable)
676+
677+
@classmethod
678+
def _read_graph(cls, b, r):
679+
graph = Graph()
680+
vertex_count = r.to_object(b, DataType.int, False)
681+
for _ in range(vertex_count):
682+
v_id = r.read_object(b)
683+
v_label = r.to_object(b, DataType.list, False)[0]
684+
vertex = Vertex(v_id, v_label)
685+
graph.vertices[v_id] = vertex
686+
687+
vp_count = r.to_object(b, DataType.int, False)
688+
for _ in range(vp_count):
689+
vp_id = r.read_object(b)
690+
vp_label = r.to_object(b, DataType.list, False)[0]
691+
vp_value = r.read_object(b)
692+
r.read_object(b) # discard parent
693+
vp = VertexProperty(vp_id, vp_label, vp_value, vertex)
694+
vertex.properties.append(vp)
695+
696+
meta_props = r.to_object(b, DataType.list, False)
697+
if meta_props:
698+
vp.properties.extend(meta_props)
699+
700+
edge_count = r.to_object(b, DataType.int, False)
701+
for _ in range(edge_count):
702+
e_id = r.read_object(b)
703+
e_label = r.to_object(b, DataType.list, False)[0]
704+
in_v_id = r.read_object(b)
705+
r.read_object(b) # discard in-v label
706+
out_v_id = r.read_object(b)
707+
r.read_object(b) # discard out-v label
708+
r.read_object(b) # discard parent
709+
710+
edge = Edge(e_id, graph.vertices[out_v_id], e_label, graph.vertices[in_v_id])
711+
graph.edges[e_id] = edge
712+
713+
edge_props = r.to_object(b, DataType.list, False)
714+
if edge_props:
715+
edge.properties.extend(edge_props)
716+
717+
return graph
647718

648719

649720
class VertexIO(_GraphBinaryTypeIO):

gremlin-python/src/main/python/radish/feature_steps.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
# under the License.
1818
#
1919

20+
from collections.abc import Iterable
2021
from datetime import datetime
2122
import json
2223
import re
2324
import uuid
2425
from gremlin_python.statics import long, bigdecimal
25-
from gremlin_python.structure.graph import Path, Vertex
26+
from gremlin_python.structure.graph import Path, Vertex, Graph, Edge, VertexProperty, Property
2627
from gremlin_python.process.anonymous_traversal import traversal
2728
from gremlin_python.process.graph_traversal import __
2829
from gremlin_python.process.traversal import Barrier, Cardinality, P, TextP, Pop, Scope, Column, Order, Direction, T, \
@@ -78,8 +79,12 @@ def choose_graph(step, graph_name):
7879
tagset = [tag.name for tag in step.all_tags]
7980
if not step.context.ignore:
8081
step.context.ignore = "AllowNullPropertyValues" in tagset
81-
if not step.context.ignore:
82+
83+
# ignore if we're not using graphbinary - graphson isn't implemented since that support was
84+
# meant to be temporary only. remove this entire check once that removal happens.
85+
if not step.context.ignore and not world.config.user_data["serializer"] == "application/vnd.graphbinary-v4.0":
8286
step.context.ignore = "StepSubgraph" in tagset
87+
8388
if not step.context.ignore:
8489
step.context.ignore = "StepTree" in tagset
8590
if not step.context.ignore:
@@ -188,7 +193,9 @@ def next_the_traversal(step):
188193
return
189194

190195
try:
191-
step.context.result = list(map(lambda x: _convert_results(x), step.context.traversal.next()))
196+
res = step.context.traversal.next()
197+
res_iter = [res] if (not isinstance(res, Iterable) or isinstance(res, (str, bytes))) else res
198+
step.context.result = [ _convert_results(x) for x in res_iter ]
192199
step.context.failed = False
193200
step.context.failed_message = ''
194201
except Exception as e:
@@ -240,6 +247,48 @@ def assert_result(step, characterized_as):
240247
raise ValueError("unknown data characterization of " + characterized_as)
241248

242249

250+
@then("the result should be a subgraph with the following")
251+
def assert_subgraph(step):
252+
if step.context.ignore:
253+
return
254+
255+
assert_that(step.context.failed, equal_to(False), step.context.failed_message)
256+
257+
# result should be a graph
258+
sg = step.context.result[0]
259+
assert_that(sg, instance_of(Graph))
260+
261+
# the first item in the datatable tells us what we are asserting
262+
if not getattr(step, "table", None):
263+
return
264+
column_name = next(iter(step.table[0].keys()))
265+
asserting_vertices = column_name == "vertices"
266+
267+
if asserting_vertices:
268+
expected_vertices = [_convert(line[column_name], step.context) for line in step.table]
269+
assert_that(len(sg.vertices), equal_to(len(expected_vertices)))
270+
271+
for expected in expected_vertices:
272+
assert_that(expected.id, is_in(sg.vertices))
273+
actual = sg.vertices[expected.id]
274+
assert_that(actual.label, equal_to(expected.label))
275+
276+
variable_key = "age" if actual.label == "person" else "lang"
277+
assert_that(actual["name"], equal_to(expected["name"]))
278+
assert_that(actual[variable_key], equal_to(expected[variable_key]))
279+
else:
280+
expected_edges = [_convert(line[column_name], step.context) for line in step.table]
281+
assert_that(len(sg.edges), equal_to(len(expected_edges)))
282+
283+
for expected in expected_edges:
284+
assert_that(expected.id, is_in(sg.edges))
285+
actual = sg.edges[expected.id]
286+
assert_that(actual.label, equal_to(expected.label))
287+
assert_that(actual["weight"], equal_to(expected["weight"]))
288+
assert_that(actual.outV.id, equal_to(expected.outV.id))
289+
assert_that(actual.inV.id, equal_to(expected.inV.id))
290+
291+
243292
@then("the graph should return {count:d} for count of {traversal_string:QuotedString}")
244293
def assert_side_effects(step, count, traversal_string):
245294
if step.context.ignore:

gremlin-python/src/main/python/radish/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def create_lookup_v(remote):
2727
g = traversal().with_(remote)
2828

2929
# hold a map of name/vertex for use in asserting results
30-
return g.V().group().by('name').by(__.tail()).next()
30+
return g.with_("materializeProperties", "all").V().group().by('name').by(__.tail()).next()
3131

3232

3333
@pick
@@ -37,7 +37,7 @@ def create_lookup_e(remote):
3737
# hold a map of the "name"/edge for use in asserting results - "name" in this context is in the form of
3838
# outgoingV-label->incomingV
3939
edges = {}
40-
edge_map = g.E().group(). \
40+
edge_map = g.with_("materializeProperties", "all").E().group(). \
4141
by(__.project('o', 'l', 'i').by(__.out_v().values('name')).by(__.label()).by(__.in_v().values('name'))). \
4242
by(__.tail()).next()
4343

gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727
from gremlin_python.process.traversal import TraversalStrategy, P, Order, T, DT, GValue, Cardinality
2828
from gremlin_python.process.graph_traversal import __
2929
from gremlin_python.process.anonymous_traversal import traversal
30-
from gremlin_python.structure.graph import Vertex
30+
from gremlin_python.structure.graph import Vertex, Edge, Graph
3131
from gremlin_python.process.strategies import SubgraphStrategy, SeedStrategy, ReservedKeysVerificationStrategy
3232
from gremlin_python.structure.io.util import HashableDict
3333
from gremlin_python.driver.protocol import GremlinServerError
34-
from gremlin_python.driver import serializer
3534

3635
gremlin_server_url = os.environ.get('GREMLIN_SERVER_URL', 'http://localhost:{}/')
3736
test_no_auth_url = gremlin_server_url.format(45940)
@@ -214,13 +213,19 @@ def test_traversals(self, remote_connection):
214213
assert len(p.objects[1].properties) == 0
215214
assert len(p.objects[2].properties) == 0
216215
# #
217-
# test materializeProperties in Path - 'all' should materialize properties on each element
218-
# p = g.with_("materializeProperties", "all").V().has('name', 'marko').outE().inV().has_label('software').path().next()
219-
# assert 3 == len(p.objects)
220-
# assert p.objects[0].properties is not None and len(p.objects[0].properties) > 0
221-
# # edges have dict-like properties; ensure not empty
222-
# assert p.objects[1].properties is not None and len(p.objects[1].properties) > 0
223-
# assert p.objects[2].properties is not None and len(p.objects[2].properties) > 0
216+
# subgraph - skipping GraphSON for now. we can remove this carve-out when we remove the GraphSON support which
217+
# was meant to be temporary
218+
if not isinstance(remote_connection._client._response_serializer, serializer.GraphSONSerializersV4):
219+
sg = g.E().has_label('knows').subgraph('sg').cap('sg').next()
220+
assert isinstance(sg, Graph)
221+
assert len(sg.vertices) == 3
222+
assert len(sg.edges) == 2
223+
for v in sg.vertices.values():
224+
assert isinstance(v, Vertex)
225+
assert v.label == 'person'
226+
for e in sg.edges.values():
227+
assert isinstance(e, Edge)
228+
assert e.label == 'knows'
224229

225230
def test_iteration(self, remote_connection):
226231
statics.load_statics(globals())

gremlin-python/src/main/python/tests/structure/io/test_graphbinaryV4.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from datetime import datetime, timedelta, timezone
2525
from gremlin_python.statics import long, bigint, BigDecimal, SingleByte, SingleChar
26-
from gremlin_python.structure.graph import Vertex, Edge, Property, VertexProperty, Path
26+
from gremlin_python.structure.graph import Graph, Vertex, Edge, Property, VertexProperty, Path
2727
from gremlin_python.structure.io.graphbinaryV4 import GraphBinaryWriter, GraphBinaryReader
2828
from gremlin_python.process.traversal import Direction
2929
from gremlin_python.structure.io.util import Marker
@@ -235,3 +235,42 @@ def test_marker(self):
235235
x = Marker.end_of_stream()
236236
output = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
237237
assert x == output
238+
239+
def test_graph(self):
240+
graph = Graph()
241+
v1 = Vertex(1, "person")
242+
v2 = Vertex(2, "person")
243+
graph.vertices[1] = v1
244+
graph.vertices[2] = v2
245+
e1 = Edge(3, v1, "knows", v2)
246+
graph.edges[3] = e1
247+
248+
# Add some properties
249+
vp1 = VertexProperty(4, "name", "marko", v1)
250+
v1.properties.append(vp1)
251+
vp1.properties.append(Property("acl", "public", vp1))
252+
253+
e1.properties.append(Property("weight", 0.5, e1))
254+
255+
output = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(graph))
256+
257+
assert isinstance(output, Graph)
258+
assert len(output.vertices) == 2
259+
assert len(output.edges) == 1
260+
261+
rv1 = output.vertices[1]
262+
assert rv1.label == "person"
263+
assert len(rv1.properties) == 1
264+
rvp1 = rv1.properties[0]
265+
assert rvp1.value == "marko"
266+
assert len(rvp1.properties) == 1
267+
assert rvp1.properties[0].key == "acl"
268+
assert rvp1.properties[0].value == "public"
269+
270+
re1 = output.edges[3]
271+
assert re1.label == "knows"
272+
assert re1.outV.id == 1
273+
assert re1.inV.id == 2
274+
assert len(re1.properties) == 1
275+
assert re1.properties[0].key == "weight"
276+
assert re1.properties[0].value == 0.5

gremlin-python/src/main/python/tests/structure/test_graph.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,32 @@ def test_path(self):
132132
assert hash(path) == hash(path2)
133133
assert path != Path([set(["a"]), set(["c", "b"]), set([])], [1, Vertex(1), "hello"])
134134
assert path != Path([set(["a", "b"]), set(["c", "b"]), set([])], [3, Vertex(1), "hello"])
135+
136+
def test_element_value_values(self):
137+
v = Vertex(1, "person", [VertexProperty(10, "name", "marko", Vertex(1)),
138+
VertexProperty(11, "age", 29, Vertex(1))])
139+
assert v["name"] == "marko"
140+
assert v["age"] == 29
141+
try:
142+
x = v["nonexistent"]
143+
assert False, "Should have thrown KeyError"
144+
except KeyError:
145+
pass
146+
147+
assert v.values("name") == ["marko"]
148+
assert v.values("age") == [29]
149+
assert "marko" in v.values()
150+
assert 29 in v.values()
151+
assert len(v.values()) == 2
152+
assert v.values("name", "age") == ["marko", 29]
153+
assert v.values("nonexistent") == []
154+
155+
e = Edge(2, Vertex(1), "knows", Vertex(3), [Property("weight", 0.5, None)])
156+
assert e["weight"] == 0.5
157+
assert e.values("weight") == [0.5]
158+
assert e.values() == [0.5]
159+
160+
vp = VertexProperty(10, "name", "marko", Vertex(1), [Property("acl", "public", None)])
161+
assert vp["acl"] == "public"
162+
assert vp.values("acl") == ["public"]
163+
assert vp.values() == ["public"]

0 commit comments

Comments
 (0)