gremlinpython 4.0.0b2__py3-none-any.whl → 4.0.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gremlin_python/__init__.py +1 -1
- gremlin_python/driver/aiohttp/transport.py +257 -85
- gremlin_python/driver/auth.py +46 -21
- gremlin_python/driver/client.py +89 -61
- gremlin_python/driver/connection.py +123 -19
- gremlin_python/driver/driver_remote_connection.py +16 -39
- gremlin_python/driver/{transport.py → exceptions.py} +10 -24
- gremlin_python/driver/http_request.py +65 -0
- gremlin_python/driver/request.py +3 -2
- gremlin_python/driver/resultset.py +11 -5
- gremlin_python/driver/serializer.py +50 -130
- gremlin_python/driver/transaction.py +178 -0
- gremlin_python/driver/useragent.py +1 -1
- gremlin_python/process/graph_traversal.py +210 -93
- gremlin_python/process/traversal.py +114 -51
- gremlin_python/structure/graph.py +450 -5
- gremlin_python/structure/io/graphbinaryV4.py +177 -37
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/METADATA +7 -5
- gremlinpython-4.0.0.dev1.dist-info/RECORD +33 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/WHEEL +1 -1
- gremlin_python/driver/protocol.py +0 -175
- gremlin_python/structure/io/graphsonV4.py +0 -599
- gremlinpython-4.0.0b2.dist-info/RECORD +0 -33
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/licenses/NOTICE +0 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/top_level.txt +0 -0
|
@@ -26,7 +26,7 @@ class Graph(object):
|
|
|
26
26
|
self.edges = {}
|
|
27
27
|
|
|
28
28
|
def __repr__(self):
|
|
29
|
-
return "graph[]"
|
|
29
|
+
return "graph[vertices:" + str(len(self.vertices)) + " edges:" + str(len(self.edges)) + "]"
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class Element(object):
|
|
@@ -64,19 +64,37 @@ class Element(object):
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class Vertex(Element):
|
|
67
|
-
def __init__(self, id, label="vertex", properties=None):
|
|
68
|
-
|
|
67
|
+
def __init__(self, id, label="vertex", properties=None, labels=None):
|
|
68
|
+
if labels is not None:
|
|
69
|
+
self._labels = set(labels)
|
|
70
|
+
Element.__init__(self, id, next(iter(labels)) if labels else "", properties)
|
|
71
|
+
else:
|
|
72
|
+
self._labels = {label} if label else {"vertex"}
|
|
73
|
+
Element.__init__(self, id, label or "vertex", properties)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def labels(self):
|
|
77
|
+
return frozenset(self._labels)
|
|
69
78
|
|
|
70
79
|
def __repr__(self):
|
|
71
80
|
return "v[" + str(self.id) + "]"
|
|
72
81
|
|
|
73
82
|
|
|
74
83
|
class Edge(Element):
|
|
75
|
-
def __init__(self, id, outV, label, inV, properties=None):
|
|
76
|
-
|
|
84
|
+
def __init__(self, id, outV, label, inV, properties=None, labels=None):
|
|
85
|
+
if labels is not None:
|
|
86
|
+
self._labels = set(labels)
|
|
87
|
+
Element.__init__(self, id, next(iter(labels)) if labels else "", properties)
|
|
88
|
+
else:
|
|
89
|
+
self._labels = {label} if label else {"edge"}
|
|
90
|
+
Element.__init__(self, id, label or "edge", properties)
|
|
77
91
|
self.outV = outV
|
|
78
92
|
self.inV = inV
|
|
79
93
|
|
|
94
|
+
@property
|
|
95
|
+
def labels(self):
|
|
96
|
+
return frozenset(self._labels)
|
|
97
|
+
|
|
80
98
|
def __repr__(self):
|
|
81
99
|
return "e[" + str(self.id) + "][" + str(self.outV.id) + "-" + self.label + "->" + str(self.inV.id) + "]"
|
|
82
100
|
|
|
@@ -141,3 +159,430 @@ class Path(object):
|
|
|
141
159
|
|
|
142
160
|
def __len__(self):
|
|
143
161
|
return len(self.objects)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class CompositePDT(object):
|
|
165
|
+
def __init__(self, name, fields):
|
|
166
|
+
if not name:
|
|
167
|
+
raise ValueError("name cannot be null or empty")
|
|
168
|
+
self._name = name
|
|
169
|
+
self._fields = dict(fields) if fields else {}
|
|
170
|
+
if any(not isinstance(k, str) for k in self._fields):
|
|
171
|
+
raise TypeError("CompositePDT field keys must be strings")
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def name(self):
|
|
175
|
+
return self._name
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def fields(self):
|
|
179
|
+
return self._fields
|
|
180
|
+
|
|
181
|
+
def __eq__(self, other):
|
|
182
|
+
return isinstance(other, CompositePDT) and self._name == other._name and self._fields == other._fields
|
|
183
|
+
|
|
184
|
+
def __hash__(self):
|
|
185
|
+
try:
|
|
186
|
+
return hash((self._name, frozenset(self._fields.items())))
|
|
187
|
+
except TypeError:
|
|
188
|
+
return hash(self._name)
|
|
189
|
+
|
|
190
|
+
def __repr__(self):
|
|
191
|
+
return f"pdt[{self._name}]{self._fields}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class PrimitivePDT(object):
|
|
195
|
+
"""An immutable primitive provider-defined type consisting of a name and an opaque string value."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, name, value):
|
|
198
|
+
if not name:
|
|
199
|
+
raise ValueError("name cannot be null or empty")
|
|
200
|
+
if value is None:
|
|
201
|
+
raise ValueError("value cannot be null")
|
|
202
|
+
self._name = name
|
|
203
|
+
self._value = value
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def name(self):
|
|
207
|
+
return self._name
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def value(self):
|
|
211
|
+
return self._value
|
|
212
|
+
|
|
213
|
+
def __eq__(self, other):
|
|
214
|
+
return isinstance(other, PrimitivePDT) and self._name == other._name and self._value == other._value
|
|
215
|
+
|
|
216
|
+
def __hash__(self):
|
|
217
|
+
return hash((self._name, self._value))
|
|
218
|
+
|
|
219
|
+
def __repr__(self):
|
|
220
|
+
return f"pdt[{self._name}]({self._value})"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class PDTRegistry(object):
|
|
224
|
+
def __init__(self):
|
|
225
|
+
self._composite_adapters_by_name = {}
|
|
226
|
+
self._composite_adapters_by_class = {}
|
|
227
|
+
self._primitive_adapters_by_name = {}
|
|
228
|
+
self._primitive_adapters_by_class = {}
|
|
229
|
+
|
|
230
|
+
def register(self, type_name, deserialize_fn, serialize_fn=None, target_class=None):
|
|
231
|
+
self._composite_adapters_by_name[type_name] = {
|
|
232
|
+
'deserialize': deserialize_fn,
|
|
233
|
+
'serialize': serialize_fn,
|
|
234
|
+
'target_class': target_class
|
|
235
|
+
}
|
|
236
|
+
if target_class is not None:
|
|
237
|
+
self._composite_adapters_by_class[target_class] = {
|
|
238
|
+
'type_name': type_name,
|
|
239
|
+
'serialize': serialize_fn,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def register_primitive(self, type_name, from_value, to_value=None, target_class=None):
|
|
243
|
+
"""Register a primitive PDT adapter.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
type_name: The PDT type name string.
|
|
247
|
+
from_value: Callable(str) -> object for deserialization.
|
|
248
|
+
to_value: Callable(object) -> str for serialization (optional).
|
|
249
|
+
target_class: The Python class this adapter produces (optional).
|
|
250
|
+
"""
|
|
251
|
+
self._primitive_adapters_by_name[type_name] = {
|
|
252
|
+
'from_value': from_value,
|
|
253
|
+
'to_value': to_value,
|
|
254
|
+
'target_class': target_class
|
|
255
|
+
}
|
|
256
|
+
if target_class is not None:
|
|
257
|
+
self._primitive_adapters_by_class[target_class] = {
|
|
258
|
+
'type_name': type_name,
|
|
259
|
+
'to_value': to_value,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def create(cls):
|
|
264
|
+
"""Create a registry populated by entry_points discovery.
|
|
265
|
+
|
|
266
|
+
Providers register adapters via pyproject.toml:
|
|
267
|
+
[project.entry-points."tinkerpop.pdt"]
|
|
268
|
+
my_types = "my_package:register_pdt_types"
|
|
269
|
+
|
|
270
|
+
Each entry point should be a callable that accepts a registry and registers adapters.
|
|
271
|
+
"""
|
|
272
|
+
import sys
|
|
273
|
+
registry = cls()
|
|
274
|
+
if sys.version_info >= (3, 10):
|
|
275
|
+
from importlib.metadata import entry_points
|
|
276
|
+
eps = entry_points(group='tinkerpop.pdt')
|
|
277
|
+
else:
|
|
278
|
+
from importlib.metadata import entry_points
|
|
279
|
+
all_eps = entry_points()
|
|
280
|
+
eps = all_eps.get('tinkerpop.pdt', [])
|
|
281
|
+
|
|
282
|
+
for ep in eps:
|
|
283
|
+
try:
|
|
284
|
+
factory = ep.load()
|
|
285
|
+
factory(registry)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
import logging
|
|
288
|
+
logging.getLogger(__name__).warning(
|
|
289
|
+
f"Failed to load PDT adapter from entry point '{ep.name}': {e}")
|
|
290
|
+
return registry
|
|
291
|
+
|
|
292
|
+
def hydrate(self, pdt):
|
|
293
|
+
"""Attempt to hydrate a CompositePDT. Returns typed object or raw PDT."""
|
|
294
|
+
if not isinstance(pdt, CompositePDT):
|
|
295
|
+
return pdt
|
|
296
|
+
|
|
297
|
+
# Always recurse into fields to hydrate nested registered PDTs.
|
|
298
|
+
changed = False
|
|
299
|
+
hydrated_fields = {}
|
|
300
|
+
for k, v in pdt.fields.items():
|
|
301
|
+
if isinstance(v, CompositePDT):
|
|
302
|
+
h = self.hydrate(v)
|
|
303
|
+
if h is not v:
|
|
304
|
+
changed = True
|
|
305
|
+
hydrated_fields[k] = h
|
|
306
|
+
elif isinstance(v, PrimitivePDT):
|
|
307
|
+
h = self.hydrate_primitive(v)
|
|
308
|
+
if h is not v:
|
|
309
|
+
changed = True
|
|
310
|
+
hydrated_fields[k] = h
|
|
311
|
+
else:
|
|
312
|
+
hydrated_fields[k] = v
|
|
313
|
+
|
|
314
|
+
adapter = self._composite_adapters_by_name.get(pdt.name)
|
|
315
|
+
if adapter is None:
|
|
316
|
+
return CompositePDT(pdt.name, hydrated_fields) if changed else pdt
|
|
317
|
+
try:
|
|
318
|
+
return adapter['deserialize'](hydrated_fields)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
import logging
|
|
321
|
+
logging.getLogger(__name__).warning(f"PDT hydration failed for '{pdt.name}': {e}")
|
|
322
|
+
return pdt
|
|
323
|
+
|
|
324
|
+
def hydrate_primitive(self, pdt):
|
|
325
|
+
"""Attempt to hydrate a PrimitivePDT. Returns typed object or raw PDT."""
|
|
326
|
+
if not isinstance(pdt, PrimitivePDT):
|
|
327
|
+
return pdt
|
|
328
|
+
adapter = self._primitive_adapters_by_name.get(pdt.name)
|
|
329
|
+
if adapter is None:
|
|
330
|
+
return pdt
|
|
331
|
+
try:
|
|
332
|
+
return adapter['from_value'](pdt.value)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
import logging
|
|
335
|
+
logging.getLogger(__name__).warning(f"Primitive PDT hydration failed for '{pdt.name}': {e}")
|
|
336
|
+
return pdt
|
|
337
|
+
|
|
338
|
+
def get_composite_adapter_by_class(self, cls):
|
|
339
|
+
"""Return (type_name, serialize_fn) tuple for the given class, or None."""
|
|
340
|
+
return self._composite_adapters_by_class.get(cls)
|
|
341
|
+
|
|
342
|
+
def get_primitive_adapter_by_class(self, cls):
|
|
343
|
+
"""Return adapter dict for the given class, or None."""
|
|
344
|
+
return self._primitive_adapters_by_class.get(cls)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# Module-level registry of @provider_defined decorated classes keyed by PDT name.
|
|
348
|
+
_pdt_decorated_types = {}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def provider_defined(name=None, included_fields=None, excluded_fields=None):
|
|
352
|
+
"""Decorator that marks a class as a Provider Defined Type."""
|
|
353
|
+
def decorator(cls):
|
|
354
|
+
cls._pdt_name = name or cls.__name__
|
|
355
|
+
cls._pdt_included_fields = included_fields
|
|
356
|
+
cls._pdt_excluded_fields = excluded_fields
|
|
357
|
+
_pdt_decorated_types[cls._pdt_name] = cls
|
|
358
|
+
return cls
|
|
359
|
+
return decorator
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class Tree(object):
|
|
363
|
+
"""
|
|
364
|
+
A tree data structure with a tree-shaped public API.
|
|
365
|
+
|
|
366
|
+
Children are backed by an ordered list of ``(key, subtree)`` entries,
|
|
367
|
+
preserving insertion order and using value-equality (``==``) for key lookup
|
|
368
|
+
so keys need not be hashable. ``None`` keys are supported.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def __init__(self, entries=None):
|
|
372
|
+
# internal ordered list of [key, subtree] pairs
|
|
373
|
+
self._entries = []
|
|
374
|
+
if entries is not None:
|
|
375
|
+
for key, child in entries:
|
|
376
|
+
if not isinstance(child, Tree):
|
|
377
|
+
raise TypeError("Tree entries must map a key to a Tree, got: " + repr(child))
|
|
378
|
+
self._entries.append([key, child])
|
|
379
|
+
|
|
380
|
+
# ------------------------------------------------------------------
|
|
381
|
+
# internal helpers
|
|
382
|
+
# ------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def _find_entry(self, key):
|
|
385
|
+
for entry in self._entries:
|
|
386
|
+
if entry[0] == key:
|
|
387
|
+
return entry
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
# ------------------------------------------------------------------
|
|
391
|
+
# navigation
|
|
392
|
+
# ------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def root_nodes(self):
|
|
395
|
+
"""Returns the list of keys at the root of this tree, in insertion order."""
|
|
396
|
+
return [entry[0] for entry in self._entries]
|
|
397
|
+
|
|
398
|
+
def child_at(self, key):
|
|
399
|
+
"""
|
|
400
|
+
Returns the child subtree for the given key.
|
|
401
|
+
|
|
402
|
+
:raises KeyError: if no immediate child exists for the given key
|
|
403
|
+
"""
|
|
404
|
+
entry = self._find_entry(key)
|
|
405
|
+
if entry is None:
|
|
406
|
+
raise KeyError("Tree has no child for key: " + str(key))
|
|
407
|
+
return entry[1]
|
|
408
|
+
|
|
409
|
+
def has_child(self, key):
|
|
410
|
+
"""Returns True if the given key is an immediate child of this tree."""
|
|
411
|
+
return self._find_entry(key) is not None
|
|
412
|
+
|
|
413
|
+
def contains(self, value):
|
|
414
|
+
"""Returns True if the given value appears as a key anywhere in this tree (recursive)."""
|
|
415
|
+
for key, child in self._entries:
|
|
416
|
+
if key == value or child.contains(value):
|
|
417
|
+
return True
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
def find_subtree(self, key):
|
|
421
|
+
"""
|
|
422
|
+
Recursively searches the tree for the first subtree rooted at ``key`` and
|
|
423
|
+
returns it, or ``None`` if not found. Direct children are visited before
|
|
424
|
+
recursing.
|
|
425
|
+
"""
|
|
426
|
+
for k, child in self._entries:
|
|
427
|
+
if k == key:
|
|
428
|
+
return child
|
|
429
|
+
for _, child in self._entries:
|
|
430
|
+
found = child.find_subtree(key)
|
|
431
|
+
if found is not None:
|
|
432
|
+
return found
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
def get_or_create_child(self, key):
|
|
436
|
+
"""Returns the existing child for ``key``, or inserts and returns a new empty Tree if absent."""
|
|
437
|
+
entry = self._find_entry(key)
|
|
438
|
+
if entry is None:
|
|
439
|
+
child = Tree()
|
|
440
|
+
self._entries.append([key, child])
|
|
441
|
+
return child
|
|
442
|
+
return entry[1]
|
|
443
|
+
|
|
444
|
+
# ------------------------------------------------------------------
|
|
445
|
+
# structural
|
|
446
|
+
# ------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
def is_leaf(self):
|
|
449
|
+
"""Returns True if this tree has no children. An empty tree is considered a leaf."""
|
|
450
|
+
return len(self._entries) == 0
|
|
451
|
+
|
|
452
|
+
def node_count(self):
|
|
453
|
+
"""Returns the total number of nodes (keys) in the tree, counted recursively."""
|
|
454
|
+
count = len(self._entries)
|
|
455
|
+
for _, child in self._entries:
|
|
456
|
+
count += child.node_count()
|
|
457
|
+
return count
|
|
458
|
+
|
|
459
|
+
def get_nodes_at_depth(self, depth):
|
|
460
|
+
"""
|
|
461
|
+
Returns the keys at the given depth. Depth 0 returns the root keys.
|
|
462
|
+
|
|
463
|
+
Negative depths and depths beyond the tree's height return an empty list.
|
|
464
|
+
"""
|
|
465
|
+
nodes = []
|
|
466
|
+
for tree in self.get_trees_at_depth(depth):
|
|
467
|
+
nodes.extend(tree.root_nodes())
|
|
468
|
+
return nodes
|
|
469
|
+
|
|
470
|
+
def get_trees_at_depth(self, depth):
|
|
471
|
+
"""
|
|
472
|
+
Returns the trees at the given depth. Depth 0 returns a singleton list
|
|
473
|
+
containing this tree. Negative depths and depths beyond the tree's
|
|
474
|
+
height return an empty list.
|
|
475
|
+
"""
|
|
476
|
+
if depth < 0:
|
|
477
|
+
return []
|
|
478
|
+
current = [self]
|
|
479
|
+
for _ in range(depth):
|
|
480
|
+
nxt = []
|
|
481
|
+
for tree in current:
|
|
482
|
+
nxt.extend(entry[1] for entry in tree._entries)
|
|
483
|
+
if not nxt:
|
|
484
|
+
return []
|
|
485
|
+
current = nxt
|
|
486
|
+
return current
|
|
487
|
+
|
|
488
|
+
def get_leaf_nodes(self):
|
|
489
|
+
"""Returns all keys whose subtrees are leaves (recursive)."""
|
|
490
|
+
leaves = []
|
|
491
|
+
self._collect_leaf_keys(leaves)
|
|
492
|
+
return leaves
|
|
493
|
+
|
|
494
|
+
def _collect_leaf_keys(self, out):
|
|
495
|
+
for key, child in self._entries:
|
|
496
|
+
if child.is_leaf():
|
|
497
|
+
out.append(key)
|
|
498
|
+
else:
|
|
499
|
+
child._collect_leaf_keys(out)
|
|
500
|
+
|
|
501
|
+
def get_leaf_trees(self):
|
|
502
|
+
"""Returns single-key trees representing each leaf key in this tree (recursive)."""
|
|
503
|
+
leaves = []
|
|
504
|
+
self._collect_leaf_trees(leaves)
|
|
505
|
+
return leaves
|
|
506
|
+
|
|
507
|
+
def _collect_leaf_trees(self, out):
|
|
508
|
+
for key, child in self._entries:
|
|
509
|
+
if child.is_leaf():
|
|
510
|
+
out.append(Tree([(key, child)]))
|
|
511
|
+
else:
|
|
512
|
+
child._collect_leaf_trees(out)
|
|
513
|
+
|
|
514
|
+
# ------------------------------------------------------------------
|
|
515
|
+
# composition
|
|
516
|
+
# ------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
def add_tree(self, other):
|
|
519
|
+
"""
|
|
520
|
+
Recursively merges ``other`` into this tree. For overlapping keys (by
|
|
521
|
+
value-equality) child subtrees are merged in turn. For keys present only
|
|
522
|
+
in ``other`` the corresponding subtree reference is adopted directly.
|
|
523
|
+
"""
|
|
524
|
+
for key, child in other._entries:
|
|
525
|
+
entry = self._find_entry(key)
|
|
526
|
+
if entry is None:
|
|
527
|
+
self._entries.append([key, child])
|
|
528
|
+
else:
|
|
529
|
+
entry[1].add_tree(child)
|
|
530
|
+
|
|
531
|
+
def split_parents(self):
|
|
532
|
+
"""
|
|
533
|
+
Splits this tree into one tree per root key. If the tree has a single
|
|
534
|
+
root, returns a singleton list containing this tree.
|
|
535
|
+
"""
|
|
536
|
+
if len(self._entries) == 1:
|
|
537
|
+
return [self]
|
|
538
|
+
return [Tree([(key, child)]) for key, child in self._entries]
|
|
539
|
+
|
|
540
|
+
# ------------------------------------------------------------------
|
|
541
|
+
# output
|
|
542
|
+
# ------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
def pretty_print(self):
|
|
545
|
+
"""
|
|
546
|
+
Produces a formatted string representation of the tree structure using a
|
|
547
|
+
``|--`` ASCII style, matching Java's ``Tree.prettyPrint``.
|
|
548
|
+
|
|
549
|
+
Each level is indented by 3 spaces relative to its parent. The returned
|
|
550
|
+
string has no trailing newline.
|
|
551
|
+
"""
|
|
552
|
+
lines = []
|
|
553
|
+
self._pretty_print(lines, "")
|
|
554
|
+
return "\n".join(lines)
|
|
555
|
+
|
|
556
|
+
def _pretty_print(self, lines, prefix):
|
|
557
|
+
for key, child in self._entries:
|
|
558
|
+
lines.append(prefix + "|--" + str(key))
|
|
559
|
+
child._pretty_print(lines, prefix + " ")
|
|
560
|
+
|
|
561
|
+
# ------------------------------------------------------------------
|
|
562
|
+
# identity
|
|
563
|
+
# ------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
def __eq__(self, other):
|
|
566
|
+
if not isinstance(other, Tree):
|
|
567
|
+
return NotImplemented
|
|
568
|
+
if len(self._entries) != len(other._entries):
|
|
569
|
+
return False
|
|
570
|
+
for key, child in self._entries:
|
|
571
|
+
matched = False
|
|
572
|
+
for okey, ochild in other._entries:
|
|
573
|
+
if okey == key:
|
|
574
|
+
if ochild != child:
|
|
575
|
+
return False
|
|
576
|
+
matched = True
|
|
577
|
+
break
|
|
578
|
+
if not matched:
|
|
579
|
+
return False
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
def __hash__(self):
|
|
583
|
+
# structurally-equal trees share the same number of root entries, so this
|
|
584
|
+
# is consistent with __eq__ while remaining valid for unhashable keys.
|
|
585
|
+
return hash(len(self._entries))
|
|
586
|
+
|
|
587
|
+
def __repr__(self):
|
|
588
|
+
return "{" + ", ".join(str(key) + "=" + repr(child) for key, child in self._entries) + "}"
|