Skip to content

Refactoring for new naming and syncing property methods with .NET #35

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

Merged
merged 10 commits into from
Feb 22, 2016
Merged
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
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Session API
.. autoclass:: neo4j.v1.ResultSummary
:members:

.. autoclass:: neo4j.v1.StatementStatistics
.. autoclass:: neo4j.v1.Counters
:members:


Expand Down
53 changes: 28 additions & 25 deletions examples/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ def test_minimal_working_example(self):

session.run("CREATE (neo:Person {name:'Neo', age:23})")

cursor = session.run("MATCH (p:Person) WHERE p.name = 'Neo' RETURN p.age")
while cursor.next():
print("Neo is %d years old." % cursor["p.age"])
result = session.run("MATCH (p:Person) WHERE p.name = 'Neo' RETURN p.age")
while result.next():
print("Neo is %d years old." % result["p.age"])

session.close()
# end::minimal-example[]
Expand Down Expand Up @@ -104,34 +104,34 @@ def test_result_cursor(self):
session = driver.session()
# tag::result-cursor[]
search_term = "hammer"
cursor = session.run("MATCH (tool:Tool) WHERE tool.name CONTAINS {term} "
result = session.run("MATCH (tool:Tool) WHERE tool.name CONTAINS {term} "
"RETURN tool.name", {"term": search_term})
print("List of tools called %r:" % search_term)
while cursor.next():
print(cursor["tool.name"])
while result.next():
print(result["tool.name"])
# end::result-cursor[]
session.close()

def test_cursor_nesting(self):
driver = GraphDatabase.driver("bolt://localhost")
session = driver.session()
# tag::retain-result-query[]
cursor = session.run("MATCH (person:Person) WHERE person.dept = {dept} "
result = session.run("MATCH (person:Person) WHERE person.dept = {dept} "
"RETURN id(person) AS minion", {"dept": "IT"})
while cursor.next():
while result.next():
session.run("MATCH (person) WHERE id(person) = {id} "
"MATCH (boss:Person) WHERE boss.name = {boss} "
"CREATE (person)-[:REPORTS_TO]->(boss)", {"id": cursor["minion"], "boss": "Bob"})
"CREATE (person)-[:REPORTS_TO]->(boss)", {"id": result["minion"], "boss": "Bob"})
# end::retain-result-query[]
session.close()

def test_result_retention(self):
driver = GraphDatabase.driver("bolt://localhost")
session = driver.session()
# tag::retain-result-process[]
cursor = session.run("MATCH (person:Person) WHERE person.dept = {dept} "
result = session.run("MATCH (person:Person) WHERE person.dept = {dept} "
"RETURN id(person) AS minion", {"dept": "IT"})
minion_records = list(cursor.stream())
minion_records = list(result.stream())

for record in minion_records:
session.run("MATCH (person) WHERE id(person) = {id} "
Expand All @@ -148,10 +148,10 @@ def test_transaction_commit(self):
tx.run("CREATE (p:Person {name: 'The One'})")
tx.commit()
# end::transaction-commit[]
cursor = session.run("MATCH (p:Person {name: 'The One'}) RETURN count(p)")
assert cursor.next()
assert cursor["count(p)"] == 1
assert cursor.at_end()
result = session.run("MATCH (p:Person {name: 'The One'}) RETURN count(p)")
assert result.next()
assert result["count(p)"] == 1
assert result.at_end
session.close()

def test_transaction_rollback(self):
Expand All @@ -162,30 +162,33 @@ def test_transaction_rollback(self):
tx.run("CREATE (p:Person {name: 'The One'})")
tx.rollback()
# end::transaction-rollback[]
cursor = session.run("MATCH (p:Person {name: 'The One'}) RETURN count(p)")
assert cursor.next()
assert cursor["count(p)"] == 0
assert cursor.at_end()
result = session.run("MATCH (p:Person {name: 'The One'}) RETURN count(p)")
assert result.next()
assert result["count(p)"] == 0
assert result.at_end
session.close()

def test_result_summary_query_profile(self):
driver = GraphDatabase.driver("bolt://localhost")
session = driver.session()
# tag::result-summary-query-profile[]
cursor = session.run("PROFILE MATCH (p:Person {name: {name}}) "
result = session.run("PROFILE MATCH (p:Person {name: {name}}) "
"RETURN id(p)", {"name": "The One"})
summary = cursor.summarize()
print(summary.statement_type)
print(summary.profile)
while result.next():
pass # skip the records to get to the summary
print(result.summary.statement_type)
print(result.summary.profile)
# end::result-summary-query-profile[]
session.close()

def test_result_summary_notifications(self):
driver = GraphDatabase.driver("bolt://localhost")
session = driver.session()
# tag::result-summary-notifications[]
summary = session.run("EXPLAIN MATCH (a), (b) RETURN a,b").summarize()
for notification in summary.notifications:
result = session.run("EXPLAIN MATCH (a), (b) RETURN a,b")
while result.next():
pass # skip the records to get to the summary
for notification in result.summary.notifications:
print(notification)
# end::result-summary-notifications[]
session.close()
4 changes: 2 additions & 2 deletions neo4j/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def main():
stdout.write("%s\r\n" % "\t".join(map(repr, record)))
if has_results:
stdout.write("\r\n")
if args.summarize:
summary = cursor.summarize()
if args.summary:
summary = cursor.summary
stdout.write("Statement : %r\r\n" % summary.statement)
stdout.write("Parameters : %r\r\n" % summary.parameters)
stdout.write("Statement Type : %r\r\n" % summary.statement_type)
Expand Down
7 changes: 7 additions & 0 deletions neo4j/v1/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,10 @@ def __init__(self, data):
for key, value in data.items():
if not key.startswith("_"):
setattr(self, key, value)


class ResultError(Exception):
""" Raised when the cursor encounters a problem.
"""

pass
28 changes: 18 additions & 10 deletions neo4j/v1/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class which can be used to obtain `Driver` instances that are used for

from .compat import integer, string, urlparse
from .connection import connect, Response, RUN, PULL_ALL
from .exceptions import CypherError
from .exceptions import CypherError, ResultError
from .typesystem import hydrated


Expand Down Expand Up @@ -170,11 +170,13 @@ def record(self):
"""
return self._current

@property
def position(self):
""" Return the current cursor position.
"""
return self._position

@property
def at_end(self):
""" Return ``True`` if at the end of the record stream, ``False``
otherwise.
Expand All @@ -185,7 +187,7 @@ def at_end(self):
return True
else:
self._connection.fetch()
return self.at_end()
return self.at_end

def stream(self):
""" Yield all subsequent records.
Expand Down Expand Up @@ -216,13 +218,19 @@ def get(self, item, default=None):
except (IndexError, KeyError):
return default

def summarize(self):
""" Consume the remainder of this result and produce a summary.
@property
def summary(self):
""" Return the summary from the trailing metadata. Note that this is
only available once the entire result stream has been consumed.
Attempting to access the summary before then will raise an error.

:rtype: ResultSummary
:raises ResultError: if the entire result has not yet been consumed
"""
self._consume()
return self._summary
if self._consumed:
return self._summary
else:
raise ResultError("Summary not available until the entire result has been consumed")

def _consume(self):
# Consume the remainder of this result, triggering all appropriate callback functions.
Expand Down Expand Up @@ -262,8 +270,8 @@ class ResultSummary(object):
#: The type of statement (``'r'`` = read-only, ``'rw'`` = read/write).
statement_type = None

#: A set of statistical information held in a :class:`.StatementStatistics` instance.
statistics = None
#: A set of statistical information held in a :class:`.Counters` instance.
counters = None

#: A :class:`.Plan` instance
plan = None
Expand All @@ -281,7 +289,7 @@ def __init__(self, statement, parameters, **metadata):
self.statement = statement
self.parameters = parameters
self.statement_type = metadata.get("type")
self.statistics = StatementStatistics(metadata.get("stats", {}))
self.counters = Counters(metadata.get("stats", {}))
if "plan" in metadata:
self.plan = make_plan(metadata["plan"])
if "profile" in metadata:
Expand All @@ -296,7 +304,7 @@ def __init__(self, statement, parameters, **metadata):
notification["description"], position))


class StatementStatistics(object):
class Counters(object):
""" Set of statistics from a Cypher statement execution.
"""

Expand Down
2 changes: 1 addition & 1 deletion neokit
1 change: 1 addition & 0 deletions runtests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ echo "Running tests with $(python --version)"
pip install --upgrade -r ${DRIVER_HOME}/test_requirements.txt
echo ""
TEST_RUNNER="coverage run -m ${UNITTEST} discover -vfs ${TEST}"
EXAMPLES_RUNNER="coverage run -m ${UNITTEST} discover -vfs examples"
BEHAVE_RUNNER="behave --tags=-db --tags=-in_dev test/tck"
if [ ${RUNNING} -eq 1 ]
then
Expand Down
78 changes: 53 additions & 25 deletions test/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from unittest import TestCase

from mock import patch
from neo4j.v1.exceptions import ResultError
from neo4j.v1.session import GraphDatabase, CypherError, Record, record
from neo4j.v1.typesystem import Node, Relationship, Path

Expand Down Expand Up @@ -85,7 +86,9 @@ def test_sessions_are_not_reused_if_still_in_use(self):
def test_can_run_simple_statement(self):
session = GraphDatabase.driver("bolt://localhost").session()
count = 0
for record in session.run("RETURN 1 AS n").stream():
cursor = session.run("RETURN 1 AS n")
assert cursor.position == -1
for record in cursor.stream():
assert record[0] == 1
assert record["n"] == 1
with self.assertRaises(KeyError):
Expand All @@ -97,6 +100,7 @@ def test_can_run_simple_statement(self):
_ = record[object()]
assert repr(record)
assert len(record) == 1
assert cursor.position == count
count += 1
session.close()
assert count == 1
Expand Down Expand Up @@ -191,25 +195,59 @@ def test_can_handle_cypher_error(self):
with self.assertRaises(CypherError):
session.run("X").close()

def test_can_obtain_summary_info(self):
def test_keys_are_available_before_and_after_stream(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("UNWIND range(1, 10) AS n RETURN n")
assert list(cursor.keys()) == ["n"]
_ = list(cursor.stream())
assert list(cursor.keys()) == ["n"]

def test_keys_with_an_error(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("X")
with self.assertRaises(CypherError):
_ = list(cursor.keys())


class SummaryTestCase(TestCase):

def test_can_obtain_summary_after_consuming_result(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("CREATE (n) RETURN n")
summary = cursor.summarize()
list(cursor.stream())
summary = cursor.summary
assert summary.statement == "CREATE (n) RETURN n"
assert summary.parameters == {}
assert summary.statement_type == "rw"
assert summary.statistics.nodes_created == 1
assert summary.counters.nodes_created == 1

def test_cannot_obtain_summary_without_consuming_result(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("CREATE (n) RETURN n")
with self.assertRaises(ResultError):
_ = cursor.summary

# def test_can_obtain_summary_immediately_if_empty_result(self):
# with GraphDatabase.driver("bolt://localhost").session() as session:
# cursor = session.run("CREATE (n)")
# summary = cursor.summary
# assert summary.statement == "CREATE (n)"
# assert summary.parameters == {}
# assert summary.statement_type == "rw"
# assert summary.counters.nodes_created == 1

def test_no_plan_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("CREATE (n) RETURN n")
assert cursor.summarize().plan is None
assert cursor.summarize().profile is None
list(cursor.stream())
assert cursor.summary.plan is None
assert cursor.summary.profile is None

def test_can_obtain_plan_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("EXPLAIN CREATE (n) RETURN n")
plan = cursor.summarize().plan
list(cursor.stream())
plan = cursor.summary.plan
assert plan.operator_type == "ProduceResults"
assert plan.identifiers == ["n"]
assert plan.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
Expand All @@ -220,7 +258,8 @@ def test_can_obtain_plan_info(self):
def test_can_obtain_profile_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("PROFILE CREATE (n) RETURN n")
profile = cursor.summarize().profile
list(cursor.stream())
profile = cursor.summary.profile
assert profile.db_hits == 0
assert profile.rows == 1
assert profile.operator_type == "ProduceResults"
Expand All @@ -232,14 +271,16 @@ def test_can_obtain_profile_info(self):

def test_no_notification_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("CREATE (n) RETURN n")
notifications = result.summarize().notifications
cursor = session.run("CREATE (n) RETURN n")
list(cursor.stream())
notifications = cursor.summary.notifications
assert notifications == []

def test_can_obtain_notification_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
notifications = result.summarize().notifications
cursor = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
list(cursor.stream())
notifications = cursor.summary.notifications

assert len(notifications) == 1
notification = notifications[0]
Expand All @@ -261,19 +302,6 @@ def test_can_obtain_notification_info(self):
assert position.line == 1
assert position.column == 1

def test_keys_are_available_before_and_after_stream(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("UNWIND range(1, 10) AS n RETURN n")
assert list(cursor.keys()) == ["n"]
_ = list(cursor.stream())
assert list(cursor.keys()) == ["n"]

def test_keys_with_an_error(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
cursor = session.run("X")
with self.assertRaises(CypherError):
_ = list(cursor.keys())


class ResetTestCase(TestCase):

Expand Down