Skip to content

Add packstream support for element_id fields #671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 81 additions & 26 deletions neo4j/graph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

from collections.abc import Mapping

from ..meta import deprecated


class Graph:
""" Local, self-contained graph object that acts as a container for
Expand Down Expand Up @@ -71,35 +73,61 @@ class Hydrator:
def __init__(self, graph):
self.graph = graph

def hydrate_node(self, n_id, n_labels=None, properties=None):
def hydrate_node(self, id_, labels=None,
properties=None, element_id=None):
assert isinstance(self.graph, Graph)
# backwards compatibility with Neo4j < 5.0
if element_id is None:
element_id = str(id_)

try:
inst = self.graph._nodes[n_id]
inst = self.graph._nodes[element_id]
except KeyError:
inst = self.graph._nodes[n_id] = Node(self.graph, n_id, n_labels, properties)
inst = self.graph._nodes[element_id] = Node(
self.graph, element_id, id_, labels, properties
)
else:
# If we have already hydrated this node as the endpoint of
# a relationship, it won't have any labels or properties.
# Therefore, we need to add the ones we have here.
if n_labels:
inst._labels = inst._labels.union(n_labels) # frozen_set
if labels:
inst._labels = inst._labels.union(labels) # frozen_set
if properties:
inst._properties.update(properties)
return inst

def hydrate_relationship(self, r_id, n0_id, n1_id, r_type, properties=None):
inst = self.hydrate_unbound_relationship(r_id, r_type, properties)
inst._start_node = self.hydrate_node(n0_id)
inst._end_node = self.hydrate_node(n1_id)
def hydrate_relationship(self, id_, n0_id, n1_id, type_,
properties=None, element_id=None,
n0_element_id=None, n1_element_id=None):
# backwards compatibility with Neo4j < 5.0
if element_id is None:
element_id = str(id_)
if n0_element_id is None:
n0_element_id = str(n0_id)
if n1_element_id is None:
n1_element_id = str(n1_id)

inst = self.hydrate_unbound_relationship(id_, type_, properties,
element_id)
inst._start_node = self.hydrate_node(n0_id,
element_id=n0_element_id)
inst._end_node = self.hydrate_node(n1_id, element_id=n1_element_id)
return inst

def hydrate_unbound_relationship(self, r_id, r_type, properties=None):
def hydrate_unbound_relationship(self, id_, type_, properties=None,
element_id=None):
assert isinstance(self.graph, Graph)
# backwards compatibility with Neo4j < 5.0
if element_id is None:
element_id = str(id_)

try:
inst = self.graph._relationships[r_id]
inst = self.graph._relationships[element_id]
except KeyError:
r = self.graph.relationship_type(r_type)
inst = self.graph._relationships[r_id] = r(self.graph, r_id, properties)
r = self.graph.relationship_type(type_)
inst = self.graph._relationships[element_id] = r(
self.graph, element_id, id_, properties
)
return inst

def hydrate_path(self, nodes, relationships, sequence):
Expand Down Expand Up @@ -131,22 +159,27 @@ class Entity(Mapping):
functionality.
"""

def __init__(self, graph, id, properties):
def __init__(self, graph, element_id, id_, properties):
self._graph = graph
self._id = id
self._properties = dict((k, v) for k, v in (properties or {}).items() if v is not None)
self._element_id = element_id
self._id = id_
self._properties = {
k: v for k, v in (properties or {}).items() if v is not None
}

def __eq__(self, other):
try:
return type(self) == type(other) and self.graph == other.graph and self.id == other.id
return (type(self) == type(other)
and self.graph == other.graph
and self.element_id == other.element_id)
except AttributeError:
return False

def __ne__(self, other):
return not self.__eq__(other)

def __hash__(self):
return hash(self.id)
return hash(self._element_id)

def __len__(self):
return len(self._properties)
Expand All @@ -167,11 +200,30 @@ def graph(self):
return self._graph

@property
@deprecated("`id` is deprecated, use `element_id` instead")
def id(self):
""" The identity of this entity in its container :class:`.Graph`.
"""The legacy identity of this entity in its container :class:`.Graph`.

Depending on the version of the server this entity was retrieved from,
this may be empty (None).

.. deprecated:: 5.0
Use :attr:`.element_id` instead.

:rtype: int
"""
return self._id

@property
def element_id(self):
"""The identity of this entity in its container :class:`.Graph`.

.. added:: 5.0

:rtype: str
"""
return self._element_id

def get(self, name, default=None):
""" Get a property value by name, optionally with a default.
"""
Expand Down Expand Up @@ -214,12 +266,14 @@ class Node(Entity):
""" Self-contained graph node.
"""

def __init__(self, graph, n_id, n_labels=None, properties=None):
Entity.__init__(self, graph, n_id, properties)
def __init__(self, graph, element_id, id_, n_labels=None,
properties=None):
Entity.__init__(self, graph, element_id, id_, properties)
self._labels = frozenset(n_labels or ())

def __repr__(self):
return "<Node id=%r labels=%r properties=%r>" % (self._id, self._labels, self._properties)
return (f"<Node element_id={self._element_id!r} "
f"labels={self._labels!r} properties={self._properties!r}>")

@property
def labels(self):
Expand All @@ -232,14 +286,15 @@ class Relationship(Entity):
""" Self-contained graph relationship.
"""

def __init__(self, graph, r_id, properties):
Entity.__init__(self, graph, r_id, properties)
def __init__(self, graph, element_id, id_, properties):
Entity.__init__(self, graph, element_id, id_, properties)
self._start_node = None
self._end_node = None

def __repr__(self):
return "<Relationship id=%r nodes=(%r, %r) type=%r properties=%r>" % (
self._id, self._start_node, self._end_node, self.type, self._properties)
return (f"<Relationship element_id={self._element_id!r} "
f"nodes={self.nodes!r} type={self.type!r} "
f"properties={self._properties!r}>")

@property
def nodes(self):
Expand Down
75 changes: 73 additions & 2 deletions tests/unit/common/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,96 @@
# limitations under the License.


import pytest

from neo4j.data import DataHydrator
from neo4j.packstream import Structure


# python -m pytest -s -v tests/unit/test_data.py


def test_can_hydrate_node_structure():
def test_can_hydrate_v1_node_structure():
hydrant = DataHydrator()

struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
alice, = hydrant.hydrate([struct])

assert alice.id == 123
with pytest.warns(DeprecationWarning, match="element_id"):
assert alice.id == 123
# for backwards compatibility, the driver should compy the element_id
assert alice.element_id == "123"
assert alice.labels == {"Person"}
assert set(alice.keys()) == {"name"}
assert alice.get("name") == "Alice"


@pytest.mark.parametrize("with_id", (True, False))
def test_can_hydrate_v2_node_structure(with_id):
hydrant = DataHydrator()

id_ = 123 if with_id else None

struct = Structure(b'N', id_, ["Person"], {"name": "Alice"}, "abc")
alice, = hydrant.hydrate([struct])

with pytest.warns(DeprecationWarning, match="element_id"):
assert alice.id == id_
assert alice.element_id == "abc"
assert alice.labels == {"Person"}
assert set(alice.keys()) == {"name"}
assert alice.get("name") == "Alice"


def test_can_hydrate_v1_relationship_structure():
hydrant = DataHydrator()

struct = Structure(b'R', 123, 456, 789, "KNOWS", {"since": 1999})
rel, = hydrant.hydrate([struct])

with pytest.warns(DeprecationWarning, match="element_id"):
assert rel.id == 123
with pytest.warns(DeprecationWarning, match="element_id"):
assert rel.start_node.id == 456
with pytest.warns(DeprecationWarning, match="element_id"):
assert rel.end_node.id == 789
# for backwards compatibility, the driver should compy the element_id
assert rel.element_id == "123"
assert rel.start_node.element_id == "456"
assert rel.end_node.element_id == "789"
assert rel.type == "KNOWS"
assert set(rel.keys()) == {"since"}
assert rel.get("since") == 1999


@pytest.mark.parametrize("with_ids", (True, False))
def test_can_hydrate_v2_relationship_structure(with_ids):
hydrant = DataHydrator()

id_ = 123 if with_ids else None
start_id = 456 if with_ids else None
end_id = 789 if with_ids else None

struct = Structure(b'R', id_, start_id, end_id, "KNOWS", {"since": 1999},
"abc", "def", "ghi")

rel, = hydrant.hydrate([struct])

with pytest.warns(DeprecationWarning, match="element_id"):
assert rel.id == id_
with pytest.warns(DeprecationWarning, match="element_id"):
assert rel.start_node.id == start_id
with pytest.warns(DeprecationWarning, match="element_id"):
assert rel.end_node.id == end_id
# for backwards compatibility, the driver should compy the element_id
assert rel.element_id == "abc"
assert rel.start_node.element_id == "def"
assert rel.end_node.element_id == "ghi"
assert rel.type == "KNOWS"
assert set(rel.keys()) == {"since"}
assert rel.get("since") == 1999


def test_hydrating_unknown_structure_returns_same():
hydrant = DataHydrator()

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/common/test_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def test_record_repr(len_):
{"x": {"one": 1, "two": 2}}
),
(
zip(["a"], [Node("graph", 42, "Person", {"name": "Alice"})]),
zip(["a"], [Node("graph", "42", 42, "Person", {"name": "Alice"})]),
(),
{"a": {"name": "Alice"}}
),
Expand Down
Loading