diff --git a/neo4j/work/simple.py b/neo4j/work/simple.py index e8c4ec8a9..9c404da4c 100644 --- a/neo4j/work/simple.py +++ b/neo4j/work/simple.py @@ -123,7 +123,10 @@ def _result_error(self, _): self._disconnect() def close(self): - """Close the session. This will release any borrowed resources, such as connections, and will roll back any outstanding transactions. + """Close the session. + + This will release any borrowed resources, such as connections, and will + roll back any outstanding transactions. """ if self._connection: if self._autoResult: diff --git a/tests/integration/test_result.py b/tests/integration/test_result.py index a6ca598b5..80e14fb2d 100644 --- a/tests/integration/test_result.py +++ b/tests/integration/test_result.py @@ -22,231 +22,24 @@ import pytest -from neo4j.exceptions import Neo4jError - - -def test_can_consume_result_immediately(session): - - def f(tx): - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - assert [record[0] for record in result] == [1, 2, 3] - - session.read_transaction(f) - - -def test_can_consume_result_from_buffer(session): - - def f(tx): - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - result._buffer_all() - assert [record[0] for record in result] == [1, 2, 3] - - session.read_transaction(f) - - -@pytest.mark.skip(reason="This behaviour have changed in 4.0. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK") -def test_can_consume_result_after_commit(session): - tx = session.begin_transaction() - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - tx.commit() - assert [record[0] for record in result] == [1, 2, 3] - - -@pytest.mark.skip(reason="This behaviour have changed in 4.0. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK") -def test_can_consume_result_after_rollback(session): - tx = session.begin_transaction() - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - tx.rollback() - assert [record[0] for record in result] == [1, 2, 3] - - -@pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK") -def test_can_consume_result_after_session_close(bolt_driver): - with bolt_driver.session() as session: - tx = session.begin_transaction() - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - tx.commit() - assert [record[0] for record in result] == [1, 2, 3] - - -@pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK") -def test_can_consume_result_after_session_reuse(bolt_driver): - session = bolt_driver.session() - tx = session.begin_transaction() - result_a = tx.run("UNWIND range(1, 3) AS n RETURN n") - tx.commit() - session.close() - session = bolt_driver.session() - tx = session.begin_transaction() - result_b = tx.run("UNWIND range(4, 6) AS n RETURN n") - tx.commit() - session.close() - assert [record[0] for record in result_a] == [1, 2, 3] - assert [record[0] for record in result_b] == [4, 5, 6] - - -def test_can_consume_results_after_harsh_session_death(bolt_driver): - session = bolt_driver.session() - result_a = session.run("UNWIND range(1, 3) AS n RETURN n") - del session - session = bolt_driver.session() - result_b = session.run("UNWIND range(4, 6) AS n RETURN n") - del session - assert [record[0] for record in result_a] == [1, 2, 3] - assert [record[0] for record in result_b] == [4, 5, 6] - - -@pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK") -def test_can_consume_result_after_session_with_error(bolt_driver): - session = bolt_driver.session() - with pytest.raises(Neo4jError): - session.run("X").consume() - session.close() - session = bolt_driver.session() - tx = session.begin_transaction() - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - tx.commit() - session.close() - assert [record[0] for record in result] == [1, 2, 3] - - -def test_single_with_exactly_one_record(session): - result = session.run("UNWIND range(1, 1) AS n RETURN n") - record = result.single() - assert list(record.values()) == [1] - - -# def test_value_with_no_records(session): -# result = session.run("CREATE ()") -# assert result.value() == [] -# -# -# def test_values_with_no_records(session): -# result = session.run("CREATE ()") -# assert result.values() == [] - - -def test_peek_can_look_one_ahead(session): - result = session.run("UNWIND range(1, 3) AS n RETURN n") - record = result.peek() - assert list(record.values()) == [1] - - -def test_peek_fails_if_nothing_remains(neo4j_driver): - with neo4j_driver.session() as session: - result = session.run("CREATE ()") - upcoming = result.peek() - assert upcoming is None - - -def test_peek_does_not_advance_cursor(session): - result = session.run("UNWIND range(1, 3) AS n RETURN n") - result.peek() - assert [record[0] for record in result] == [1, 2, 3] - - -def test_peek_at_different_stages(session): - result = session.run("UNWIND range(0, 9) AS n RETURN n") - # Peek ahead to the first record - expected_next = 0 - upcoming = result.peek() - assert upcoming[0] == expected_next - # Then look through all the other records - for expected, record in enumerate(result): - # Check this record is as expected - assert record[0] == expected - # Check the upcoming record is as expected... - if expected < 9: - # ...when one should follow - expected_next = expected + 1 - upcoming = result.peek() - assert upcoming[0] == expected_next - else: - # ...when none should follow - upcoming = result.peek() - assert upcoming is None - - -def test_can_safely_exit_session_without_consuming_result(session): - session.run("RETURN 1") - assert True - - -def test_multiple_record_value_case_a(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - values = [] - for record in result: - values.append(record.value(key=0, default=None)) - assert values == [1, 2, 3] - - -def test_multiple_record_value_case_b(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - values = [] - for record in result: - values.append(record.value(key=2, default=None)) - assert values == [3, 6, 9] - - -def test_multiple_record_value_case_c(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - values = [] - for record in result: - values.append(record.value(key="z", default=None)) - assert values == [3, 6, 9] - - -def test_record_values_case_a(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - values = [] - for record in result: - values.append(record.values()) - assert values == [[1, 2, 3], [2, 4, 6], [3, 6, 9]] - - -def test_record_values_case_b(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - values = [] - for record in result: - values.append(record.values(2, 0)) - assert values == [[3, 1], [6, 2], [9, 3]] - - -def test_record_values_case_c(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - values = [] - for record in result: - values.append(record.values("z", "x")) - assert values == [[3, 1], [6, 2], [9, 3]] - - -def test_no_records(neo4j_driver): - with neo4j_driver.session() as session: - result = session.run("CREATE ()") - values = [] - for record in result: - values.append(record.value()) - assert values == [] - - +# TODO: this test will stay until a uniform behavior for `.single()` across the +# drivers has been specified and tests are created in testkit def test_result_single_with_no_records(session): result = session.run("CREATE ()") record = result.single() assert record is None +# TODO: this test will stay until a uniform behavior for `.single()` across the +# drivers has been specified and tests are created in testkit def test_result_single_with_one_record(session): result = session.run("UNWIND [1] AS n RETURN n") record = result.single() assert record["n"] == 1 +# TODO: this test will stay until a uniform behavior for `.single()` across the +# drivers has been specified and tests are created in testkit def test_result_single_with_multiple_records(session): import warnings result = session.run("UNWIND [1, 2, 3] AS n RETURN n") @@ -255,57 +48,11 @@ def test_result_single_with_multiple_records(session): assert record[0] == 1 +# TODO: this test will stay until a uniform behavior for `.single()` across the +# drivers has been specified and tests are created in testkit def test_result_single_consumes_the_result(session): result = session.run("UNWIND [1, 2, 3] AS n RETURN n") with pytest.warns(UserWarning, match="Expected a result with a single record"): _ = result.single() records = list(result) assert records == [] - - -def test_single_value(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().value() == 1 - - -def test_single_indexed_value(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().value(2) == 3 - - -def test_single_keyed_value(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().value("z") == 3 - - -def test_single_values(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().values() == [1, 2, 3] - - -def test_single_indexed_values(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().values(2, 0) == [3, 1] - - -def test_single_keyed_values(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().values("z", "x") == [3, 1] - - -def test_result_with_helper_function_value(session): - - def f(tx): - result = tx.run("UNWIND range(1, 3) AS n RETURN n") - assert result.value(0) == [1, 2, 3] - - session.read_transaction(f) - - -def test_result_with_helper_function_values(session): - - def f(tx): - result = tx.run("UNWIND range(1, 3) AS n RETURN n, 0") - assert result.values(0, 1) == [[1, 0], [2, 0], [3, 0]] - - session.read_transaction(f) diff --git a/tests/integration/test_result_data.py b/tests/integration/test_result_data.py deleted file mode 100644 index ae545e6f8..000000000 --- a/tests/integration/test_result_data.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf-8 -*- - -# Copyright (c) "Neo4j" -# Neo4j Sweden AB [http://neo4j.com] -# -# This file is part of Neo4j. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_data_with_one_key_and_no_records(session): - result = session.run("UNWIND range(1, 0) AS n RETURN n") - data = [record.data() for record in result] - assert data == [] - - -def test_data_with_one_key_and_no_records_with_helper_function(session): - result = session.run("UNWIND range(1, 0) AS n RETURN n") - assert result.data() == [] - - -def test_multiple_data(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - data = [record.data() for record in result] - assert data == [{"x": 1, "y": 2, "z": 3}, {"x": 2, "y": 4, "z": 6}, {"x": 3, "y": 6, "z": 9}] - - -def test_multiple_data_with_helper_function(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - assert result.data() == [{"x": 1, "y": 2, "z": 3}, {"x": 2, "y": 4, "z": 6}, {"x": 3, "y": 6, "z": 9}] - - -def test_multiple_indexed_data(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - data = [record.data(2, 0) for record in result] - assert data == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}] - - -def test_multiple_indexed_data_with_helper_function(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - assert result.data(2, 0) == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}] - - -def test_multiple_keyed_data(session): - result = session.run("UNWIND range(1, 3) AS n " - "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") - data = [record.data("z", "x") for record in result] - assert data == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}] - - -def test_single_data(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().data() == {"x": 1, "y": 2, "z": 3} - - -def test_single_indexed_data(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().data(2, 0) == {"x": 1, "z": 3} - - -def test_single_keyed_data(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - assert result.single().data("z", "x") == {"x": 1, "z": 3} - - -def test_none(session): - result = session.run("RETURN null AS x") - data = [record.data() for record in result] - assert data == [{"x": None}] - - -def test_bool(session): - result = session.run("RETURN true AS x, false AS y") - data = [record.data() for record in result] - assert data == [{"x": True, "y": False}] - - -def test_int(session): - result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") - data = [record.data() for record in result] - assert data == [{"x": 1, "y": 2, "z": 3}] - - -def test_float(session): - result = session.run("RETURN 0.0 AS x, 1.0 AS y, 3.141592653589 AS z") - data = [record.data() for record in result] - assert data == [{"x": 0.0, "y": 1.0, "z": 3.141592653589}] - - -def test_string(session): - result = session.run("RETURN 'hello, world' AS x") - data = [record.data() for record in result] - assert data == [{"x": "hello, world"}] - - -def test_byte_array(session): - result = session.run("RETURN $x AS x", x=bytearray([1, 2, 3])) - data = [record.data() for record in result] - assert data == [{"x": bytearray([1, 2, 3])}] - - -def test_list(session): - result = session.run("RETURN [1, 2, 3] AS x") - data = [record.data() for record in result] - assert data == [{"x": [1, 2, 3]}] - - -def test_dict(session): - result = session.run("RETURN {one: 1, two: 2} AS x") - data = [record.data() for record in result] - assert data == [{"x": {"one": 1, "two": 2}}] - - -def test_node(session): - result = session.run("CREATE (x:Person {name:'Alice'}) RETURN x") - data = [record.data() for record in result] - assert data == [{"x": {"name": "Alice"}}] - - -def test_relationship_with_pre_known_nodes(session): - result = session.run("CREATE (a:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(b:Person {name:'Bob'}) " - "RETURN a, b, x") - data = [record.data() for record in result] - assert data == [{"a": {"name": "Alice"}, "b": {"name": "Bob"}, "x": ({"name": "Alice"}, "KNOWS", {"name": "Bob"})}] - - -def test_relationship_with_post_known_nodes(session): - result = session.run("CREATE (a:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(b:Person {name:'Bob'}) " - "RETURN x, a, b") - data = [record.data() for record in result] - assert data == [{"x": ({"name": "Alice"}, "KNOWS", {"name": "Bob"}), "a": {"name": "Alice"}, "b": {"name": "Bob"}}] - - -def test_relationship_with_unknown_nodes(session): - result = session.run("CREATE (:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(:Person {name:'Bob'}) " - "RETURN x") - data = [record.data() for record in result] - assert data == [{"x": ({}, "KNOWS", {})}] - - -def test_path(session): - result = session.run("CREATE x = (a:Person {name:'Alice'})-[:KNOWS {since:1999}]->(b:Person {name:'Bob'}) " - "RETURN x") - data = [record.data() for record in result] - assert data == [{"x": [{"name": "Alice"}, "KNOWS", {"name": "Bob"}]}] diff --git a/tests/unit/test_record.py b/tests/unit/test_record.py index 37356c548..778b7ea34 100644 --- a/tests/unit/test_record.py +++ b/tests/unit/test_record.py @@ -21,7 +21,11 @@ import pytest -from neo4j.data import Record +from neo4j.data import ( + Graph, + Node, + Record, +) # python -m pytest -s -v tests/unit/test_record.py @@ -146,6 +150,21 @@ def test_record_value(): _ = r.value(None) +def test_record_value_kwargs(): + r = Record(zip(["name", "age", "married"], ["Alice", 33, True])) + assert r.value() == "Alice" + assert r.value(key="name") == "Alice" + assert r.value(key="age") == 33 + assert r.value(key="married") is True + assert r.value(key="shoe size") is None + assert r.value(key="shoe size", default=6) == 6 + assert r.value(key=0) == "Alice" + assert r.value(key=1) == 33 + assert r.value(key=2) is True + assert r.value(key=3) is None + assert r.value(key=3, default=6) == 6 + + def test_record_contains(): r = Record(zip(["name", "age", "married"], ["Alice", 33, True])) assert "Alice" in r @@ -202,3 +221,113 @@ def test_record_len(len_): def test_record_repr(len_): r = Record(("key_%i" % i, "val_%i" % i) for i in range(len_)) assert repr(r) + + +@pytest.mark.parametrize(("raw", "keys", "serialized"), ( + ( + zip(["x", "y", "z"], [1, 2, 3]), + (), + {"x": 1, "y": 2, "z": 3} + ), + ( + zip(["x", "y", "z"], [1, 2, 3]), + (1, 2), + {"y": 2, "z": 3} + ), + ( + zip(["x", "y", "z"], [1, 2, 3]), + ("z", "x"), + {"x": 1, "z": 3} + ), + ( + zip(["x"], [None]), + (), + {"x": None} + ), + ( + zip(["x", "y"], [True, False]), + (), + {"x": True, "y": False} + ), + ( + zip(["x", "y", "z"], [0.0, 1.0, 3.141592653589]), + (), + {"x": 0.0, "y": 1.0, "z": 3.141592653589} + ), + ( + zip(["x"], ["hello, world"]), + (), + {"x": "hello, world"} + ), + ( + zip(["x"], [bytearray([1, 2, 3])]), + (), + {"x": bytearray([1, 2, 3])} + ), + ( + zip(["x"], [[1, 2, 3]]), + (), + {"x": [1, 2, 3]} + ), + ( + zip(["x"], [{"one": 1, "two": 2}]), + (), + {"x": {"one": 1, "two": 2}} + ), + ( + zip(["a"], [Node("graph", 42, "Person", {"name": "Alice"})]), + (), + {"a": {"name": "Alice"}} + ), +)) +def test_data(raw, keys, serialized): + assert Record(raw).data(*keys) == serialized + + +def test_data_relationship(): + g = Graph() + gh = Graph.Hydrator(g) + alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33}) + bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44}) + alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", + {"since": 1999}) + record = Record(zip(["a", "b", "r"], [alice, bob, alice_knows_bob])) + assert record.data() == { + "a": {"name": "Alice", "age": 33}, + "b": {"name": "Bob", "age": 44}, + "r": ( + {"name": "Alice", "age": 33}, + "KNOWS", + {"name": "Bob", "age": 44} + ), + } + + +def test_data_unbound_relationship(): + g = Graph() + gh = Graph.Hydrator(g) + some_one_knows_some_one = gh.hydrate_relationship( + 1, 42, 43, "KNOWS", {"since": 1999} + ) + record = Record(zip(["r"], [some_one_knows_some_one])) + assert record.data() == {"r": ({}, "KNOWS", {})} + + +@pytest.mark.parametrize("cyclic", (True, False)) +def test_data_path(cyclic): + g = Graph() + gh = Graph.Hydrator(g) + alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33}) + bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44}) + if cyclic: + carol = alice + else: + carol = gh.hydrate_node(3, {"Person"}, {"name": "Carol", "age": 55}) + r = [gh.hydrate_unbound_relationship(1, "KNOWS", {"since": 1999}), + gh.hydrate_unbound_relationship(2, "DISLIKES", {})] + path = gh.hydrate_path([alice, bob, carol], r, [1, 1, -2, 2]) + + record = Record(zip(["r"], [path])) + assert record.data() == { + "r": [dict(alice), "KNOWS", dict(bob), "DISLIKES", dict(carol)] + } diff --git a/tests/unit/work/test_result.py b/tests/unit/work/test_result.py index 3fe6a92c8..c4b5aa13d 100644 --- a/tests/unit/work/test_result.py +++ b/tests/unit/work/test_result.py @@ -19,6 +19,8 @@ # limitations under the License. +from unittest import mock + import pytest from neo4j import ( @@ -216,8 +218,12 @@ def _fetch_and_compare_all_records(result, key, expected_records, method, @pytest.mark.parametrize("method", ("for loop", "next", "new iter")) -def test_result_iteration(method): - records = [[1], [2], [3], [4], [5]] +@pytest.mark.parametrize("records", ( + [], + [[42]], + [[1], [2], [3], [4], [5]], +)) +def test_result_iteration(method, records): connection = ConnectionStub(records=Records(["x"], records)) result = Result(connection, HydratorStub(), 2, noop, noop) result._run("CYPHER", {}, None, None, "r", None) @@ -395,3 +401,24 @@ def test_query_type(query_type): assert isinstance(summary.query_type, str) assert summary.query_type == query_type + + +@pytest.mark.parametrize("num_records", range(0, 5)) +def test_data(num_records): + connection = ConnectionStub( + records=Records(["n"], [[i + 1] for i in range(num_records)]) + ) + + result = Result(connection, HydratorStub(), 1, noop, noop) + result._run("CYPHER", {}, None, None, "r", None) + result._buffer_all() + records = result._record_buffer.copy() + assert len(records) == num_records + expected_data = [] + for i, record in enumerate(records): + record.data = mock.Mock() + expected_data.append("magic_return_%s" % i) + record.data.return_value = expected_data[-1] + assert result.data("hello", "world") == expected_data + for record in records: + assert record.data.called_once_with("hello", "world")