diff --git a/make_docs.sh b/bin/make-docs
similarity index 53%
rename from make_docs.sh
rename to bin/make-docs
index 2d8b6ea7b..bdd68394f 100755
--- a/make_docs.sh
+++ b/bin/make-docs
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
-HOME=$(dirname $0)
+ROOT=$(dirname "$0")/..
-pip install --upgrade sphinx
-make -C ${HOME}/docs html
+pip install --quiet --upgrade sphinx
+make -C ${ROOT}/docs html
echo ""
-INDEX_FILE="${HOME}/docs/build/html/index.html"
+INDEX_FILE="${ROOT}/docs/build/html/index.html"
echo "Documentation index file can be found at file://$(cd "$(dirname "${INDEX_FILE}")"; pwd)/$(basename "${INDEX_FILE}")"
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8385cd243..06a8a235f 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -37,7 +37,6 @@
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.ifconfig',
- 'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
]
diff --git a/docs/source/driver.rst b/docs/source/driver.rst
index 63d7f15a0..d3105bb8a 100644
--- a/docs/source/driver.rst
+++ b/docs/source/driver.rst
@@ -13,10 +13,10 @@ Construction
:class:`.Driver` construction can either be carried out directly or via a `classmethod` on the :class:`.GraphDatabase` class.
.. autoclass:: neo4j.GraphDatabase
- :members:
+ :members: driver
.. autoclass:: neo4j.Driver(uri, **config)
- :members:
+ :members: session, close, closed
URI
@@ -37,14 +37,7 @@ URI scheme:
Driver subclass:
:class:`.DirectDriver`
-A Bolt :class:`.DirectDriver` is used to target a single machine.
-This may be a standalone server or could be a specific member of a cluster.
-
-Connections established by a :class:`.DirectDriver` are always made to the exact host and port detailed in the URI.
-
.. autoclass:: neo4j.DirectDriver
- :members:
- :inherited-members:
Bolt Routing
@@ -56,8 +49,6 @@ Driver subclass:
:class:`.RoutingDriver`
.. autoclass:: neo4j.RoutingDriver
- :members:
- :inherited-members:
Configuration
diff --git a/docs/source/errors.rst b/docs/source/errors.rst
index ff2f5982a..e31815174 100644
--- a/docs/source/errors.rst
+++ b/docs/source/errors.rst
@@ -2,26 +2,42 @@
Errors
******
-.. autoclass:: neo4j.exceptions.CypherError
- :members:
-.. autoclass:: neo4j.exceptions.ProtocolError
- :members:
+Connectivity errors
+===================
-.. autoclass:: neo4j.exceptions.SecurityError
- :members:
+.. class:: neo4j.exceptions.ServiceUnavailable
-.. autoclass:: neo4j.exceptions.ServiceUnavailable
- :members:
+ Raised when a database server or service is not available.
+ This may be due to incorrect configuration or could indicate a runtime failure of a database service that the driver is unable to route around.
+
+.. class:: neo4j.exceptions.SecurityError
+
+ Raised when a security issue occurs, generally around TLS or authentication.
+
+
+Cypher execution errors
+=======================
+
+.. class:: neo4j.exceptions.CypherError
+
+ Raised when the Cypher engine returns an error to the client.
+ There are many possible types of Cypher error, each identified by a unique `status code `_.
+
+ The three classifications of status code are supported by the three subclasses of :class:`.CypherError`, listed below:
.. autoclass:: neo4j.exceptions.ClientError
- :show-inheritance:
- :members:
.. autoclass:: neo4j.exceptions.DatabaseError
- :show-inheritance:
- :members:
.. autoclass:: neo4j.exceptions.TransientError
- :show-inheritance:
- :members:
+
+
+Low-level errors
+================
+
+.. class:: neo4j.exceptions.ProtocolError
+
+ Raised when an unexpected or unsupported protocol event occurs.
+ This error generally indicates a fault with the driver or server software.
+ If you receive this error, please raise a GitHub issue or a support ticket.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index da7244e32..52c1685ba 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -2,7 +2,8 @@
Neo4j Bolt Driver |version| for Python
**************************************
-The Official Neo4j Driver for Python supports Neo4j 3.1 and above and requires Python version 2.7, 3.4, 3.5 or 3.6.
+The Official Neo4j Driver for Python supports Neo4j 3.2 and above and requires Python version 2.7 or 3.4+.
+Note that support for Python 2.7 will be removed in the 2.0 driver.
Quick Example
@@ -25,20 +26,25 @@ Quick Example
session.read_transaction(print_friends_of, "Alice")
+.. note::
+
+ While imports from ``neo4j.v1`` still work, these will be removed in the 2.0 driver.
+ It is therefore recommended to change all imports from ``neo4j.v1`` to ``neo4j``.
+
+
Installation
============
-To install the latest stable version, use:
+To install the latest stable driver release, use:
.. code:: bash
pip install neo4j
-For the most up-to-date version (possibly unstable), use:
-
-.. code:: bash
+.. note::
- pip install git+https://github.com/neo4j/neo4j-python-driver.git#egg=neo4j
+ The driver is currently released under two package names on `PyPI `_: ``neo4j`` and ``neo4j-driver``.
+ Installing from ``neo4j`` is recommended since ``neo4j-driver`` will be removed in a future release.
API Documentation
diff --git a/docs/source/results.rst b/docs/source/results.rst
index ae081766d..da61fd4cf 100644
--- a/docs/source/results.rst
+++ b/docs/source/results.rst
@@ -9,12 +9,95 @@ Each result consists of header metadata, zero or more :class:`.Record` objects a
Results also contain a buffer that automatically stores unconsumed records when results are consumed out of order.
A :class:`.BoltStatementResult` is attached to an active connection, through a :class:`.Session`, until all its content has been buffered or consumed.
-.. autoclass:: neo4j.BoltStatementResult
- :inherited-members:
- :members:
+.. class:: neo4j.BoltStatementResult
-.. autoclass:: neo4j.Record
- :members:
+ .. describe:: iter(result)
+
+ .. autoattribute:: session
+
+ .. automethod:: attached
+
+ .. automethod:: detach
+
+ .. automethod:: keys
+
+ .. automethod:: records
+
+ .. automethod:: summary
+
+ .. automethod:: consume
+
+ .. automethod:: single
+
+ .. automethod:: peek
+
+ .. automethod:: graph
+
+ .. automethod:: value
+
+ .. automethod:: values
+
+ .. automethod:: data
+
+
+.. class:: neo4j.Record
+
+ A :class:`.Record` is an immutable ordered collection of key-value
+ pairs. It is generally closer to a :py:class:`namedtuple` than to a
+ :py:class:`OrderedDict` inasmuch as iteration of the collection will
+ yield values rather than keys.
+
+ .. describe:: Record(iterable)
+
+ Create a new record based on an dictionary-like iterable.
+ This can be a dictionary itself, or may be a sequence of key-value pairs, each represented by a tuple.
+
+ .. describe:: record == other
+
+ Compare a record for equality with another value.
+ The `other` value may be any `Sequence` or `Mapping`, or both.
+ If comparing with a `Sequence`, the values are compared in order.
+ If comparing with a `Mapping`, the values are compared based on their keys.
+ If comparing with a value that exhibits both traits, both comparisons must be true for the values to be considered equal.
+
+ .. describe:: record != other
+
+ Compare a record for inequality with another value.
+ See above for comparison rules.
+
+ .. describe:: hash(record)
+
+ Create a hash for this record.
+ This will raise a :exc:`TypeError` if any values within the record are unhashable.
+
+ .. describe:: record[index]
+
+ Obtain a value from the record by index.
+ This will raise an :exc:`IndexError` if the specified index is out of range.
+
+ .. describe:: record[i:j]
+
+ Derive a sub-record based on a start and end index.
+ All keys and values within those bounds will be copied across in the same order as in the original record.
+
+ .. describe:: record[key]
+
+ Obtain a value from the record by key.
+ This will raise a :exc:`KeyError` if the specified key does not exist.
+
+ .. automethod:: get(key, default=None)
+
+ .. automethod:: value(key=0, default=None)
+
+ .. automethod:: index(key)
+
+ .. automethod:: keys
+
+ .. automethod:: values
+
+ .. automethod:: items
+
+ .. automethod:: data
Summary Details
diff --git a/docs/source/transactions.rst b/docs/source/transactions.rst
index 967781bf7..957f80a07 100644
--- a/docs/source/transactions.rst
+++ b/docs/source/transactions.rst
@@ -2,27 +2,65 @@
Sessions & Transactions
***********************
+All database activity is co-ordinated through two mechanisms: the :class:`.Session` and the :class:`.Transaction`.
A :class:`.Transaction` is a unit of work that is either committed in its entirety or is rolled back on failure.
-A :class:`.Session` is a logical container for one or more transactional units of work.
-Sessions automatically provide guarantees of causal consistency within a clustered environment.
+A :class:`.Session` is a logical container for any number of causally-related transactional units of work.
+Sessions automatically provide guarantees of causal consistency within a clustered environment but multiple sessions can also be causally chained if required.
-A session can be given a default `access mode` on construction.
-This applies only in clustered environments and determines whether transactions carried out within that session should be routed to a `read` or `write` server.
-Transaction functions within that session can override this access mode.
-.. note::
- The driver does not parse Cypher statements and cannot determine whether a statement tagged as `read` or `write` is tagged correctly.
- Since the access mode is not passed to the server, this can allow a `write` statement to be executed in a `read` call on a single instance.
- Clustered environments are not susceptible to this loophole as cluster roles prevent it.
+Sessions
+========
+
+Sessions provide the top-level of containment for database activity.
+Session creation is a lightweight operation and sessions are `not` thread safe.
+
+Connections are drawn from the :class:`.Driver` connection pool as required; an idle session will not hold onto a connection.
+
+Sessions will often be created and destroyed using a `with` block context.
+For example::
+
+ with driver.session() as session:
+ result = session.run("MATCH (a:Person) RETURN a.name")
+ # do something with the result...
+
+To construct a :class:`.Session` use the :meth:`.Driver.session` method.
+
+.. class:: neo4j.Session
+
+ .. automethod:: close
+
+ .. automethod:: closed
+
+ .. automethod:: run
+
+ .. automethod:: sync
+
+ .. automethod:: detach
+
+ .. automethod:: next_bookmarks
+
+ .. automethod:: last_bookmark
+
+ .. automethod:: has_transaction
+
+ .. automethod:: begin_transaction
+
+ .. automethod:: read_transaction
+
+ .. automethod:: write_transaction
+
+
+Transactions
+============
Neo4j supports three kinds of transaction: `auto-commit transactions`, `explicit transactions` and `transaction functions`.
Each has pros and cons but if in doubt, use a transaction function.
Auto-commit Transactions
-========================
-Auto-commit transactions are the simplest form, available via :meth:`.Session.run`.
-These are fast and easy to use but support only one statement per transaction and are not automatically retried on failure.
-Auto-commit transactions are also the only way to run ``USING PERIODIC COMMIT`` statements.
+------------------------
+Auto-commit transactions are the simplest form of transaction, available via :meth:`.Session.run`.
+These are easy to use but support only one statement per transaction and are not automatically retried on failure.
+Auto-commit transactions are also the only way to run ``PERIODIC COMMIT`` statements, since this Cypher clause manages its own transactions internally.
.. code-block:: python
@@ -32,8 +70,43 @@ Auto-commit transactions are also the only way to run ``USING PERIODIC COMMIT``
"RETURN id(a)", name=name).single().value()
Explicit Transactions
-=====================
+---------------------
Explicit transactions support multiple statements and must be created with an explicit :meth:`.Session.begin_transaction` call.
+This creates a new :class:`.Transaction` object that can be used to run Cypher.
+It also gives applications the ability to directly control `commit` and `rollback` activity.
+
+.. class:: neo4j.Transaction
+
+ .. automethod:: run
+
+ .. automethod:: sync
+
+ .. attribute:: success
+
+ This attribute can be used to determine the outcome of a transaction on closure.
+ Specifically, this will be either a COMMIT or a ROLLBACK.
+ A value can be set for this attribute multiple times in user code before a transaction completes, with only the final value taking effect.
+
+ On closure, the outcome is evaluated according to the following rules:
+
+ ================ ==================== =========================== ============== =============== =================
+ :attr:`.success` ``__exit__`` cleanly ``__exit__`` with exception ``tx.close()`` ``tx.commit()`` ``tx.rollback()``
+ ================ ==================== =========================== ============== =============== =================
+ :const:`None` COMMIT ROLLBACK ROLLBACK COMMIT ROLLBACK
+ :const:`True` COMMIT COMMIT [1]_ COMMIT COMMIT ROLLBACK
+ :const:`False` ROLLBACK ROLLBACK ROLLBACK COMMIT ROLLBACK
+ ================ ==================== =========================== ============== =============== =================
+
+ .. [1] While a COMMIT will be attempted in this scenario, it will likely fail if the exception originated from Cypher execution within that transaction.
+
+ .. automethod:: close
+
+ .. automethod:: closed
+
+ .. automethod:: commit
+
+ .. automethod:: rollback
+
Closing an explicit transaction can either happen automatically at the end of a ``with`` block, using the :attr:`.Transaction.success` attribute to determine success,
or can be explicitly controlled through the :meth:`.Transaction.commit` and :meth:`.Transaction.rollback` methods.
Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction.
@@ -56,7 +129,7 @@ Explicit transactions are most useful for applications that need to distribute C
"SET a.name = $name", id=node_id, name=name)
Transaction Functions
-=====================
+---------------------
Transaction functions are the most powerful form of transaction, providing access mode override and retry capabilities.
These allow a function object representing the transactional unit of work to be passed as a parameter.
This function is called one or more times, within a configurable time limit, until it succeeds.
@@ -72,11 +145,22 @@ Returning a live result object would prevent the driver from correctly managing
with driver.session() as session:
node_id = session.write_transaction(create_person, "Alice")
+To exert more control over how a transaction function is carried out, the :func:`.unit_of_work` decorator can be used.
+
+.. autofunction:: neo4j.unit_of_work
-API
-===
-.. autoclass:: neo4j.Session
- :members:
-.. autoclass:: neo4j.Transaction
- :members:
+Access modes
+============
+
+A session can be given a default `access mode` on construction.
+This applies only in clustered environments and determines whether transactions carried out within that session should be routed to a `read` or `write` server by default.
+
+Note that this mode is simply a default and not a constraint.
+This means that transaction functions within a session can override the access mode passed to that session on construction.
+
+.. note::
+ The driver does not parse Cypher statements and cannot determine whether a statement tagged as `read` or `write` is tagged correctly.
+ Since the access mode is not passed to the server, this can allow a `write` statement to be executed in a `read` call on a single instance.
+ Clustered environments are not susceptible to this loophole as cluster roles prevent it.
+ This behaviour should not be relied upon as the loophole may be closed in a future release.
diff --git a/docs/source/types/graph.rst b/docs/source/types/graph.rst
index aa1701767..b7e7ec22a 100644
--- a/docs/source/types/graph.rst
+++ b/docs/source/types/graph.rst
@@ -4,11 +4,10 @@ Graph Data Types
Cypher queries can return entire graph structures as well as individual property values.
-, The graph types model graph data returned from a Cypher query.
+The graph data types detailed here model graph data returned from a Cypher query.
Graph values cannot be passed in as parameters as it would be unclear whether the entity was intended to be passed by reference or by value.
The identity or properties of that entity should be passed explicitly instead.
-All graph values returned within a given :class:`.StatementResult` are contained within a :class:`.Graph` instance, accessible via :meth:`.StatementResult.graph`.
The driver contains a corresponding class for each of the graph types that can be returned.
============= ======================
@@ -19,22 +18,149 @@ Relationship :class:`.Relationship`
Path :class:`.Path`
============= ======================
-.. autoclass:: neo4j.types.graph.Graph
- :members:
+.. class:: neo4j.types.graph.Graph
-.. autoclass:: neo4j.types.graph.Entity
- :members:
+ A local, self-contained graph object that acts as a container for :class:`.Node` and :class:`.Relationship` instances.
+ This is typically obtained via the :meth:`.BoltStatementResult.graph` method.
-.. autoclass:: neo4j.types.graph.EntitySetView
- :members:
+ .. autoattribute:: nodes
-.. autoclass:: neo4j.types.graph.Node
- :members:
- :inherited-members:
+ .. autoattribute:: relationships
-.. autoclass:: neo4j.types.graph.Relationship
- :members:
- :inherited-members:
+ .. automethod:: relationship_type
-.. autoclass:: neo4j.types.graph.Path
- :members:
+
+.. class:: neo4j.types.graph.Node
+
+ .. describe:: node == other
+
+ Compares nodes for equality.
+
+ .. describe:: node != other
+
+ Compares nodes for inequality.
+
+ .. describe:: hash(node)
+
+ Computes the hash of a node.
+
+ .. describe:: len(node)
+
+ Returns the number of properties on a node.
+
+ .. describe:: iter(node)
+
+ Iterates through all properties on a node.
+
+ .. describe:: node[key]
+
+ Returns a node property by key.
+ Raises :exc:`KeyError` if the key does not exist.
+
+ .. describe:: key in node
+
+ Checks whether a property key exists for a given node.
+
+ .. autoattribute:: graph
+
+ .. autoattribute:: id
+
+ .. autoattribute:: labels
+
+ .. automethod:: get
+
+ .. automethod:: keys
+
+ .. automethod:: values
+
+ .. automethod:: items
+
+
+.. class:: neo4j.types.graph.Relationship
+
+ .. describe:: relationship == other
+
+ Compares relationships for equality.
+
+ .. describe:: relationship != other
+
+ Compares relationships for inequality.
+
+ .. describe:: hash(relationship)
+
+ Computes the hash of a relationship.
+
+ .. describe:: len(relationship)
+
+ Returns the number of properties on a relationship.
+
+ .. describe:: iter(relationship)
+
+ Iterates through all properties on a relationship.
+
+ .. describe:: relationship[key]
+
+ Returns a relationship property by key.
+ Raises :exc:`KeyError` if the key does not exist.
+
+ .. describe:: key in relationship
+
+ Checks whether a property key exists for a given relationship.
+
+ .. describe:: type(relationship)
+
+ Returns the type (class) of a relationship.
+ Relationship objects belong to a custom subtype based on the type name in the underlying database.
+
+ .. autoattribute:: graph
+
+ .. autoattribute:: id
+
+ .. autoattribute:: nodes
+
+ .. autoattribute:: start_node
+
+ .. autoattribute:: end_node
+
+ .. autoattribute:: type
+
+ .. automethod:: get
+
+ .. automethod:: keys
+
+ .. automethod:: values
+
+ .. automethod:: items
+
+
+.. class:: neo4j.types.graph.Path
+
+ .. describe:: path == other
+
+ Compares paths for equality.
+
+ .. describe:: path != other
+
+ Compares paths for inequality.
+
+ .. describe:: hash(path)
+
+ Computes the hash of a path.
+
+ .. describe:: len(path)
+
+ Returns the number of relationships in a path.
+
+ .. describe:: iter(path)
+
+ Iterates through all the relationships in a path.
+
+ .. autoattribute:: graph
+
+ .. autoattribute:: nodes
+
+ .. autoattribute:: start_node
+
+ .. autoattribute:: end_node
+
+ .. autoattribute:: relationships
diff --git a/neo4j/__init__.py b/neo4j/__init__.py
index b7ea1e2de..42257bf69 100644
--- a/neo4j/__init__.py
+++ b/neo4j/__init__.py
@@ -73,7 +73,7 @@
from warnings import warn
-from .compat import perf_counter, urlparse, xstr, Mapping
+from .compat import perf_counter, urlparse, xstr, Sequence, Mapping
from .config import *
from .meta import version as __version__
@@ -170,8 +170,7 @@ def session(self, access_mode=None, **parameters):
raise DriverError("Driver closed")
def close(self):
- """ Shut down, closing any open connections that were spawned by
- this :class:`.Driver`.
+ """ Shut down, closing any open connections in the pool.
"""
if not self._closed:
self._closed = True
@@ -180,13 +179,18 @@ def close(self):
self._pool = None
def closed(self):
+ """ Return :const:`True` if closed, :const:`False` otherwise.
+ """
return self._closed
class DirectDriver(Driver):
""" A :class:`.DirectDriver` is created from a ``bolt`` URI and addresses
- a single database instance. This provides basic connectivity to any
- database service topology.
+ a single database machine. This may be a standalone server or could be a
+ specific member of a cluster.
+
+ Connections established by a :class:`.DirectDriver` are always made to the
+ exact host and port detailed in the URI.
"""
uri_scheme = "bolt"
@@ -227,7 +231,8 @@ def session(self, access_mode=None, **parameters):
class RoutingDriver(Driver):
""" A :class:`.RoutingDriver` is created from a ``bolt+routing`` URI. The
- routing behaviour works in tandem with Neo4j's causal clustering feature
+ routing behaviour works in tandem with Neo4j's `Causal Clustering
+ `_ feature
by directing read and write behaviour to appropriate cluster members.
"""
@@ -701,7 +706,7 @@ def write_transaction(self, unit_of_work, *args, **kwargs):
return self._run_transaction(WRITE_ACCESS, unit_of_work, *args, **kwargs)
def _assert_open(self):
- if self.closed():
+ if self._closed:
raise SessionError("Session closed")
@@ -718,7 +723,7 @@ class Transaction(object):
#: When set, the transaction will be committed on close, otherwise it
#: will be rolled back. This attribute can be set in user code
- #: multiple times before a transaction completes with only the final
+ #: multiple times before a transaction completes, with only the final
#: value taking effect.
success = None
@@ -732,6 +737,8 @@ def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
+ if self._closed:
+ return
if self.success is None:
self.success = not bool(exc_type)
self.close()
@@ -763,9 +770,9 @@ def run(self, statement, parameters=None, **kwparameters):
:param parameters: dictionary of parameters
:param kwparameters: additional keyword parameters
:returns: :class:`.StatementResult` object
+ :raise TransactionError: if the transaction is closed
"""
- if self.closed():
- raise TransactionError("Transaction closed")
+ self._assert_open()
return self.session.run(statement, parameters, **kwparameters)
def sync(self):
@@ -774,50 +781,54 @@ def sync(self):
:raise TransactionError: if the transaction is closed
"""
- if self.closed():
- raise TransactionError("Transaction closed")
+ self._assert_open()
self.session.sync()
def commit(self):
""" Mark this transaction as successful and close in order to
- trigger a COMMIT.
+ trigger a COMMIT. This is functionally equivalent to::
+
+ tx.success = True
+ tx.close()
:raise TransactionError: if already closed
"""
- if self.closed():
- raise TransactionError("Transaction closed")
self.success = True
self.close()
def rollback(self):
""" Mark this transaction as unsuccessful and close in order to
- trigger a ROLLBACK.
+ trigger a ROLLBACK. This is functionally equivalent to::
+
+ tx.success = False
+ tx.close()
:raise TransactionError: if already closed
"""
- if self.closed():
- raise TransactionError("Transaction closed")
self.success = False
self.close()
def close(self):
- """ Close this transaction, triggering either a COMMIT or a ROLLBACK.
+ """ Close this transaction, triggering either a COMMIT or a ROLLBACK,
+ depending on the value of :attr:`.success`.
+
+ :raise TransactionError: if already closed
"""
from neobolt.exceptions import CypherError
- if not self.closed():
- try:
- self.sync()
- except CypherError:
- self.success = False
- raise
- finally:
- if self.session.has_transaction():
- if self.success:
- self.session.commit_transaction()
- else:
- self.session.rollback_transaction()
- self._closed = True
- self.on_close()
+ self._assert_open()
+ try:
+ self.sync()
+ except CypherError:
+ self.success = False
+ raise
+ finally:
+ if self.session.has_transaction():
+ if self.success:
+ self.session.commit_transaction()
+ else:
+ self.session.rollback_transaction()
+ self._closed = True
+ self.on_close()
def closed(self):
""" Indicator to show whether the transaction has been closed.
@@ -825,6 +836,10 @@ def closed(self):
"""
return self._closed
+ def _assert_open(self):
+ if self._closed:
+ raise TransactionError("Transaction closed")
+
class Statement(object):
@@ -874,13 +889,13 @@ def __iter__(self):
@property
def session(self):
+ """ The :class:`.Session` to which this result is attached, if any.
+ """
return self._session
def attached(self):
""" Indicator for whether or not this result is still attached to
- a :class:`.Session`.
-
- :returns: :const:`True` if still attached, :const:`False` otherwise
+ an open :class:`.Session`.
"""
return self._session and not self._session.closed()
@@ -1235,7 +1250,22 @@ def __repr__(self):
" ".join("%s=%r" % (field, self[i]) for i, field in enumerate(self.__keys)))
def __eq__(self, other):
- return dict(self) == dict(other)
+ """ In order to be flexible regarding comparison, the equality rules
+ for a record permit comparison with any other Sequence or Mapping.
+
+ :param other:
+ :return:
+ """
+ compare_as_sequence = isinstance(other, Sequence)
+ compare_as_mapping = isinstance(other, Mapping)
+ if compare_as_sequence and compare_as_mapping:
+ return list(self) == list(other) and dict(self) == dict(other)
+ elif compare_as_sequence:
+ return list(self) == list(other)
+ elif compare_as_mapping:
+ return dict(self) == dict(other)
+ else:
+ return False
def __ne__(self, other):
return not self.__eq__(other)
@@ -1261,6 +1291,13 @@ def __getslice__(self, start, stop):
return self.__class__(zip(keys, values))
def get(self, key, default=None):
+ """ Obtain a value from the record by key, returning a default
+ value if the key does not exist.
+
+ :param key:
+ :param default:
+ :return:
+ """
try:
index = self.__keys.index(ustr(key))
except ValueError:
@@ -1272,6 +1309,9 @@ def get(self, key, default=None):
def index(self, key):
""" Return the index of the given item.
+
+ :param key:
+ :return:
"""
if isinstance(key, integer):
if 0 <= key < len(self.__keys):
@@ -1407,7 +1447,15 @@ def __init__(self, transaction, *args, **kwargs):
def unit_of_work(metadata=None, timeout=None):
- """ Decorator for transaction functions.
+ """ This function is a decorator for transaction functions that allows
+ extra control over how the transaction is carried out.
+
+ For example, a timeout (in seconds) may be applied::
+
+ @unit_of_work(timeout=25.0)
+ def count_people(tx):
+ return tx.run("MATCH (a:Person) RETURN count(a)").single().value()
+
"""
def wrapper(f):
diff --git a/neo4j/compat/__init__.py b/neo4j/compat/__init__.py
index 92f4ad62c..95f9ca4d6 100644
--- a/neo4j/compat/__init__.py
+++ b/neo4j/compat/__init__.py
@@ -124,9 +124,9 @@ def perf_counter():
# Using or importing the ABCs from 'collections' instead of from
# 'collections.abc' is deprecated, and in 3.8 it will stop working
try:
- from collections.abc import Mapping
+ from collections.abc import Sequence, Mapping
except ImportError:
- from collections import Mapping
+ from collections import Sequence, Mapping
# The location of urlparse varies between Python 2 and 3
diff --git a/neo4j/types/graph.py b/neo4j/types/graph.py
index 66419a897..91f50311e 100644
--- a/neo4j/types/graph.py
+++ b/neo4j/types/graph.py
@@ -49,15 +49,13 @@ def __init__(self):
@property
def nodes(self):
- """ Access an :class:`.EntitySetView` of the nodes in this
- graph.
+ """ Access a set view of the nodes in this graph.
"""
return self._node_set_view
@property
def relationships(self):
- """ Access an :class:`.EntitySetView` of the relationships in
- this graph.
+ """ Access a set view of the relationships in this graph.
"""
return self._relationship_set_view
@@ -201,6 +199,8 @@ def __repr__(self):
@property
def labels(self):
+ """ The set of labels attached to this node.
+ """
return frozenset(self._labels)
@@ -224,18 +224,27 @@ def __repr__(self):
@property
def nodes(self):
+ """ The pair of nodes which this relationship connects.
+ """
return self._start_node, self._end_node
@property
def start_node(self):
+ """ The start node of this relationship.
+ """
return self._start_node
@property
def end_node(self):
+ """ The end node of this relationship.
+ """
return self._end_node
@property
def type(self):
+ """ The type name of this relationship.
+ This is functionally equivalent to ``type(relationship).__name__``.
+ """
return type(self).__name__
@property
@@ -294,22 +303,32 @@ def __iter__(self):
@property
def graph(self):
+ """ The :class:`.Graph` to which this path belongs.
+ """
return self._nodes[0].graph
@property
def nodes(self):
+ """ The sequence of :class:`.Node` objects in this path.
+ """
return self._nodes
@property
def start_node(self):
+ """ The first :class:`.Node` in this path.
+ """
return self._nodes[0]
@property
def end_node(self):
+ """ The last :class:`.Node` in this path.
+ """
return self._nodes[-1]
@property
def relationships(self):
+ """ The sequence of :class:`.Relationship` objects in this path.
+ """
return self._relationships
@property
diff --git a/test/integration/test_result.py b/test/integration/test_result.py
index dee3ba14f..21dba4c9e 100644
--- a/test/integration/test_result.py
+++ b/test/integration/test_result.py
@@ -272,14 +272,14 @@ def test_single_consumes_entire_result_if_one_record(self):
session = self.driver.session()
result = session.run("UNWIND range(1, 1) AS n RETURN n")
_ = result.single()
- assert not result.attached()
+ assert not result.session
def test_single_consumes_entire_result_if_multiple_records(self):
session = self.driver.session()
result = session.run("UNWIND range(1, 3) AS n RETURN n")
with pytest.warns(UserWarning):
_ = result.single()
- assert not result.attached()
+ assert not result.session
def test_single_value(self):
with self.driver.session() as session:
diff --git a/test/integration/test_session.py b/test/integration/test_session.py
index 50b6a3d23..9665e9f4a 100644
--- a/test/integration/test_session.py
+++ b/test/integration/test_session.py
@@ -441,6 +441,14 @@ def test_transaction_timeout(self):
with self.assertRaises(TransientError):
tx2.run("MATCH (a:Node) SET a.property = 2").consume()
+ def test_exit_after_explicit_close_should_be_silent(self):
+ with self.driver.session() as s:
+ with s.begin_transaction() as tx:
+ self.assertFalse(tx.closed())
+ tx.close()
+ self.assertTrue(tx.closed())
+ self.assertTrue(tx.closed())
+
class BookmarkingTestCase(DirectIntegrationTestCase):