Skip to content

Commit c559093

Browse files
author
Zhen Li
committed
Merge pull request #52 from neo4j/1.0-pingle
Peek and single methods
2 parents 8ecfa64 + e5774dc commit c559093

File tree

4 files changed

+124
-21
lines changed

4 files changed

+124
-21
lines changed

neo4j/v1/exceptions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ class ProtocolError(Exception):
2323
""" Raised when an unexpected or unsupported protocol event occurs.
2424
"""
2525

26-
pass
27-
2826

2927
class CypherError(Exception):
3028
""" Raised when the Cypher engine returns an error to the client.
@@ -38,3 +36,8 @@ def __init__(self, data):
3836
for key, value in data.items():
3937
if not key.startswith("_"):
4038
setattr(self, key, value)
39+
40+
41+
class ResultError(Exception):
42+
""" Raised when an error occurs while consuming a result.
43+
"""

neo4j/v1/session.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class which can be used to obtain `Driver` instances that are used for
3333
from .compat import integer, string, urlparse
3434
from .connection import connect, Response, RUN, PULL_ALL
3535
from .constants import ENCRYPTED_DEFAULT, TRUST_DEFAULT, TRUST_SIGNED_CERTIFICATES
36-
from .exceptions import CypherError
36+
from .exceptions import CypherError, ResultError
3737
from .ssl_compat import SSL_AVAILABLE, SSLContext, PROTOCOL_SSLv23, OP_NO_SSLv2, CERT_REQUIRED
3838
from .types import hydrated
3939

@@ -256,6 +256,32 @@ def consume(self):
256256
self.connection = None
257257
return self._summary
258258

259+
def single(self):
260+
""" Return the next record, failing if none or more than one remain.
261+
"""
262+
records = list(self)
263+
num_records = len(records)
264+
if num_records == 0:
265+
raise ResultError("No records found in stream")
266+
elif num_records != 1:
267+
raise ResultError("Multiple records found in stream")
268+
else:
269+
return records[0]
270+
271+
def peek(self):
272+
""" Return the next record without advancing the cursor. Fails
273+
if no records remain.
274+
"""
275+
if self._buffer:
276+
values = self._buffer[0]
277+
return Record(self.keys(), tuple(map(hydrated, values)))
278+
while not self._buffer and not self._consumed:
279+
self.connection.fetch()
280+
if self._buffer:
281+
values = self._buffer[0]
282+
return Record(self.keys(), tuple(map(hydrated, values)))
283+
raise ResultError("End of stream")
284+
259285

260286
class ResultSummary(object):
261287
""" A summary of execution returned with a :class:`.StatementResult` object.

runtests.sh

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,23 @@ DRIVER_HOME=$(dirname $0)
2121

2222
NEORUN_OPTIONS=""
2323
RUNNING=0
24+
QUICK=0
2425
KNOWN_HOSTS="${HOME}/.neo4j/known_hosts"
2526
KNOWN_HOSTS_BACKUP="${KNOWN_HOSTS}.backup"
2627

2728
FG_BRIGHT_RED='\033[1;31m'
2829
FG_DEFAULT='\033[0m'
2930

3031
# Parse options
31-
while getopts ":dr" OPTION
32+
while getopts ":dqr" OPTION
3233
do
3334
case ${OPTION} in
3435
d)
3536
NEORUN_OPTIONS="-f"
3637
;;
38+
q)
39+
QUICK=1
40+
;;
3741
r)
3842
RUNNING=1
3943
;;
@@ -79,34 +83,33 @@ echo ""
7983

8084
TEST_RUNNER="coverage run -m ${UNITTEST} discover -vfs ${TEST}"
8185
EXAMPLES_RUNNER="coverage run -m ${UNITTEST} discover -vfs examples"
82-
BEHAVE_RUNNER="behave --tags=-db --tags=-in_dev --tags=-streaming_and_cursor_navigation test/tck"
86+
BEHAVE_RUNNER="behave --tags=-db --tags=-in_dev test/tck"
8387

8488
if [ ${RUNNING} -eq 1 ]
8589
then
8690
${TEST_RUNNER}
8791
check_exit_status $?
8892
else
89-
#echo "Updating password"
90-
#mv ${KNOWN_HOSTS} ${KNOWN_HOSTS_BACKUP}
91-
#neokit/neorun ${NEORUN_OPTIONS} "python -m test.auth password" ${VERSIONS}
92-
#EXIT_STATUS=$?
93-
#mv ${KNOWN_HOSTS_BACKUP} ${KNOWN_HOSTS}
94-
#check_exit_status ${EXIT_STATUS}
9593
export NEO4J_PASSWORD="password"
9694

9795
echo "Running unit tests"
9896
neokit/neorun ${NEORUN_OPTIONS} "${TEST_RUNNER}" ${VERSIONS}
9997
check_exit_status $?
10098

101-
echo "Testing example code"
102-
neokit/neorun ${NEORUN_OPTIONS} "${EXAMPLES_RUNNER}" ${VERSIONS}
103-
check_exit_status $?
99+
if [ ${QUICK} -eq 0 ]
100+
then
101+
echo "Testing example code"
102+
neokit/neorun ${NEORUN_OPTIONS} "${EXAMPLES_RUNNER}" ${VERSIONS}
103+
check_exit_status $?
104+
105+
echo "Testing TCK"
106+
coverage report --show-missing
107+
python -c 'from test.tck.configure_feature_files import *; set_up()'
108+
echo "Feature files downloaded"
109+
neokit/neorun ${NEORUN_OPTIONS} "${BEHAVE_RUNNER}" ${VERSIONS}
110+
python -c 'from test.tck.configure_feature_files import *; clean_up()'
111+
echo "Feature files removed"
104112

105-
coverage report --show-missing
106-
python -c 'from test.tck.configure_feature_files import *; set_up()'
107-
echo "Feature files downloaded"
108-
neokit/neorun ${NEORUN_OPTIONS} "${BEHAVE_RUNNER}" ${VERSIONS}
109-
python -c 'from test.tck.configure_feature_files import *; clean_up()'
110-
echo "Feature files removed"
113+
fi
111114

112115
fi

test/test_session.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from mock import patch
2727

2828
from neo4j.v1.constants import TRUST_ON_FIRST_USE
29-
from neo4j.v1.exceptions import CypherError
29+
from neo4j.v1.exceptions import CypherError, ResultError
3030
from neo4j.v1.session import GraphDatabase, basic_auth, Record, SSL_AVAILABLE
3131
from neo4j.v1.types import Node, Relationship, Path
3232

@@ -575,3 +575,74 @@ def test_can_consume_result_after_session_with_error(self):
575575
tx.commit()
576576
session.close()
577577
assert [record[0] for record in result] == [1, 2, 3]
578+
579+
def test_single_with_exactly_one_record(self):
580+
session = self.driver.session()
581+
result = session.run("UNWIND range(1, 1) AS n RETURN n")
582+
record = result.single()
583+
assert list(record.values()) == [1]
584+
585+
def test_single_with_no_records(self):
586+
session = self.driver.session()
587+
result = session.run("CREATE ()")
588+
with self.assertRaises(ResultError):
589+
result.single()
590+
591+
def test_single_with_multiple_records(self):
592+
session = self.driver.session()
593+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
594+
with self.assertRaises(ResultError):
595+
result.single()
596+
597+
def test_single_consumes_entire_result_if_one_record(self):
598+
session = self.driver.session()
599+
result = session.run("UNWIND range(1, 1) AS n RETURN n")
600+
_ = result.single()
601+
assert result._consumed
602+
603+
def test_single_consumes_entire_result_if_multiple_records(self):
604+
session = self.driver.session()
605+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
606+
with self.assertRaises(ResultError):
607+
_ = result.single()
608+
assert result._consumed
609+
610+
def test_peek_can_look_one_ahead(self):
611+
session = self.driver.session()
612+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
613+
record = result.peek()
614+
assert list(record.values()) == [1]
615+
616+
def test_peek_fails_if_nothing_remains(self):
617+
session = self.driver.session()
618+
result = session.run("CREATE ()")
619+
with self.assertRaises(ResultError):
620+
result.peek()
621+
622+
def test_peek_does_not_advance_cursor(self):
623+
session = self.driver.session()
624+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
625+
result.peek()
626+
assert [record[0] for record in result] == [1, 2, 3]
627+
628+
def test_peek_at_different_stages(self):
629+
session = self.driver.session()
630+
result = session.run("UNWIND range(0, 9) AS n RETURN n")
631+
# Peek ahead to the first record
632+
expected_next = 0
633+
upcoming = result.peek()
634+
assert upcoming[0] == expected_next
635+
# Then look through all the other records
636+
for expected, record in enumerate(result):
637+
# Check this record is as expected
638+
assert record[0] == expected
639+
# Check the upcoming record is as expected...
640+
if expected < 9:
641+
# ...when one should follow
642+
expected_next = expected + 1
643+
upcoming = result.peek()
644+
assert upcoming[0] == expected_next
645+
else:
646+
# ...when none should follow
647+
with self.assertRaises(ResultError):
648+
result.peek()

0 commit comments

Comments
 (0)