From 29e8a11a6aada2da40fabd985120f8b28f2b6847 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 22 Nov 2023 13:21:57 +0100 Subject: [PATCH 1/4] Accept transaction config for execute_query `Driver.execute_query` now accepts a `Query` object to specify transaction config like metadata and transaction timeout. Example: ```python from neo4j import ( GraphDatabase, Query, ) with GraphDatabase.driver(...) as driver: driver.execute_query( Query( "MATCH (n) RETURN n", # metadata to be logged with the transaction metadata={"foo": "bar"}, # give the transaction 5 seconds to complete on the DBMS timeout=5, ), # all the other configuration options as before database_="neo4j", # ... ) ``` --- docs/source/api.rst | 16 ++++++++----- docs/source/async_api.rst | 16 ++++++++----- src/neo4j/_async/driver.py | 38 ++++++++++++++++++++--------- src/neo4j/_sync/driver.py | 38 ++++++++++++++++++++--------- src/neo4j/_work/query.py | 8 ++++--- testkitbackend/_async/requests.py | 7 +++++- testkitbackend/_sync/requests.py | 7 +++++- tests/unit/async_/test_driver.py | 40 ++++++++++++++++++++++++++----- tests/unit/sync/test_driver.py | 40 ++++++++++++++++++++++++++----- 9 files changed, 159 insertions(+), 51 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 960f25e71..b3da41940 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -187,8 +187,9 @@ Closing a driver will immediately shut down all connections in the pool. query_, parameters_, routing_, database_, impersonated_user_, bookmark_manager_, auth_, result_transformer_, **kwargs ): + @unit_of_work(query_.metadata, query_.timeout) def work(tx): - result = tx.run(query_, parameters_, **kwargs) + result = tx.run(query_.text, parameters_, **kwargs) return result_transformer_(result) with driver.session( @@ -245,16 +246,19 @@ Closing a driver will immediately shut down all connections in the pool. assert isinstance(count, int) return count - :param query_: cypher query to execute - :type query_: typing.LiteralString + :param query_: + Cypher query to execute. + Use a :class:`.Query` object to pass a query with additional + transaction configuration. + :type query_: typing.LiteralString | Query :param parameters_: parameters to use in the query - :type parameters_: typing.Dict[str, typing.Any] | None + :type parameters_: typing.Optional[typing.Dict[str, typing.Any]] :param routing_: - whether to route the query to a reader (follower/read replica) or + Whether to route the query to a reader (follower/read replica) or a writer (leader) in the cluster. Default is to route to a writer. :type routing_: RoutingControl :param database_: - database to execute the query against. + Database to execute the query against. None (default) uses the database configured on the server side. diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 5518628e8..6f1c793cb 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -174,8 +174,9 @@ Closing a driver will immediately shut down all connections in the pool. query_, parameters_, routing_, database_, impersonated_user_, bookmark_manager_, auth_, result_transformer_, **kwargs ): + @unit_of_work(query_.metadata, query_.timeout) async def work(tx): - result = await tx.run(query_, parameters_, **kwargs) + result = await tx.run(query_.text, parameters_, **kwargs) return await result_transformer_(result) async with driver.session( @@ -232,16 +233,19 @@ Closing a driver will immediately shut down all connections in the pool. assert isinstance(count, int) return count - :param query_: cypher query to execute - :type query_: typing.LiteralString + :param query_: + Cypher query to execute. + Use a :class:`.Query` object to pass a query with additional + transaction configuration. + :type query_: typing.LiteralString | Query :param parameters_: parameters to use in the query - :type parameters_: typing.Dict[str, typing.Any] | None + :type parameters_: typing.Optional[typing.Dict[str, typing.Any]] :param routing_: - whether to route the query to a reader (follower/read replica) or + Whether to route the query to a reader (follower/read replica) or a writer (leader) in the cluster. Default is to route to a writer. :type routing_: RoutingControl :param database_: - database to execute the query against. + Database to execute the query against. None (default) uses the database configured on the server side. diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index 596c95c5a..be4e184e5 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -49,7 +49,11 @@ experimental_warn, unclosed_resource_warn, ) -from .._work import EagerResult +from .._work import ( + EagerResult, + Query, + unit_of_work, +) from ..addressing import Address from ..api import ( AsyncBookmarkManager, @@ -583,7 +587,7 @@ async def close(self) -> None: @t.overload async def execute_query( self, - query_: te.LiteralString, + query_: t.Union[te.LiteralString, Query], parameters_: t.Optional[t.Dict[str, t.Any]] = None, routing_: T_RoutingControl = RoutingControl.WRITE, database_: t.Optional[str] = None, @@ -602,7 +606,7 @@ async def execute_query( @t.overload async def execute_query( self, - query_: te.LiteralString, + query_: t.Union[te.LiteralString, Query], parameters_: t.Optional[t.Dict[str, t.Any]] = None, routing_: T_RoutingControl = RoutingControl.WRITE, database_: t.Optional[str] = None, @@ -620,7 +624,7 @@ async def execute_query( async def execute_query( self, - query_: te.LiteralString, + query_: t.Union[te.LiteralString, Query], parameters_: t.Optional[t.Dict[str, t.Any]] = None, routing_: T_RoutingControl = RoutingControl.WRITE, database_: t.Optional[str] = None, @@ -653,8 +657,9 @@ async def execute_query( query_, parameters_, routing_, database_, impersonated_user_, bookmark_manager_, auth_, result_transformer_, **kwargs ): + @unit_of_work(query_.metadata, query_.timeout) async def work(tx): - result = await tx.run(query_, parameters_, **kwargs) + result = await tx.run(query_.text, parameters_, **kwargs) return await result_transformer_(result) async with driver.session( @@ -711,16 +716,19 @@ async def example(driver: neo4j.AsyncDriver) -> int: assert isinstance(count, int) return count - :param query_: cypher query to execute - :type query_: typing.LiteralString + :param query_: + Cypher query to execute. + Use a :class:`.Query` object to pass a query with additional + transaction configuration. + :type query_: typing.LiteralString | Query :param parameters_: parameters to use in the query :type parameters_: typing.Optional[typing.Dict[str, typing.Any]] :param routing_: - whether to route the query to a reader (follower/read replica) or + Whether to route the query to a reader (follower/read replica) or a writer (leader) in the cluster. Default is to route to a writer. :type routing_: RoutingControl :param database_: - database to execute the query against. + Database to execute the query against. None (default) uses the database configured on the server side. @@ -852,6 +860,14 @@ async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: "latter case, use the `parameters_` dictionary instead." % invalid_kwargs ) + if isinstance(query_, Query): + timeout = query_.timeout + metadata = query_.metadata + query_str = query_.text + work = unit_of_work(metadata, timeout)(_work) + else: + query_str = query_ + work = _work parameters = dict(parameters_ or {}, **kwargs) if bookmark_manager_ is _default: @@ -878,7 +894,7 @@ async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: with session._pipelined_begin: return await session._run_transaction( access_mode, TelemetryAPI.DRIVER, - _work, (query_, parameters, result_transformer_), {} + work, (query_str, parameters, result_transformer_), {} ) @property @@ -1197,7 +1213,7 @@ async def _get_server_info(self, session_config) -> ServerInfo: async def _work( tx: AsyncManagedTransaction, - query: str, + query: te.LiteralString, parameters: t.Dict[str, t.Any], transformer: t.Callable[[AsyncResult], t.Awaitable[_T]] ) -> _T: diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index 44f388996..459a69b86 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -49,7 +49,11 @@ experimental_warn, unclosed_resource_warn, ) -from .._work import EagerResult +from .._work import ( + EagerResult, + Query, + unit_of_work, +) from ..addressing import Address from ..api import ( Auth, @@ -582,7 +586,7 @@ def close(self) -> None: @t.overload def execute_query( self, - query_: te.LiteralString, + query_: t.Union[te.LiteralString, Query], parameters_: t.Optional[t.Dict[str, t.Any]] = None, routing_: T_RoutingControl = RoutingControl.WRITE, database_: t.Optional[str] = None, @@ -601,7 +605,7 @@ def execute_query( @t.overload def execute_query( self, - query_: te.LiteralString, + query_: t.Union[te.LiteralString, Query], parameters_: t.Optional[t.Dict[str, t.Any]] = None, routing_: T_RoutingControl = RoutingControl.WRITE, database_: t.Optional[str] = None, @@ -619,7 +623,7 @@ def execute_query( def execute_query( self, - query_: te.LiteralString, + query_: t.Union[te.LiteralString, Query], parameters_: t.Optional[t.Dict[str, t.Any]] = None, routing_: T_RoutingControl = RoutingControl.WRITE, database_: t.Optional[str] = None, @@ -652,8 +656,9 @@ def execute_query( query_, parameters_, routing_, database_, impersonated_user_, bookmark_manager_, auth_, result_transformer_, **kwargs ): + @unit_of_work(query_.metadata, query_.timeout) def work(tx): - result = tx.run(query_, parameters_, **kwargs) + result = tx.run(query_.text, parameters_, **kwargs) return result_transformer_(result) with driver.session( @@ -710,16 +715,19 @@ def example(driver: neo4j.Driver) -> int: assert isinstance(count, int) return count - :param query_: cypher query to execute - :type query_: typing.LiteralString + :param query_: + Cypher query to execute. + Use a :class:`.Query` object to pass a query with additional + transaction configuration. + :type query_: typing.LiteralString | Query :param parameters_: parameters to use in the query :type parameters_: typing.Optional[typing.Dict[str, typing.Any]] :param routing_: - whether to route the query to a reader (follower/read replica) or + Whether to route the query to a reader (follower/read replica) or a writer (leader) in the cluster. Default is to route to a writer. :type routing_: RoutingControl :param database_: - database to execute the query against. + Database to execute the query against. None (default) uses the database configured on the server side. @@ -851,6 +859,14 @@ def example(driver: neo4j.Driver) -> neo4j.Record:: "latter case, use the `parameters_` dictionary instead." % invalid_kwargs ) + if isinstance(query_, Query): + timeout = query_.timeout + metadata = query_.metadata + query_str = query_.text + work = unit_of_work(metadata, timeout)(_work) + else: + query_str = query_ + work = _work parameters = dict(parameters_ or {}, **kwargs) if bookmark_manager_ is _default: @@ -877,7 +893,7 @@ def example(driver: neo4j.Driver) -> neo4j.Record:: with session._pipelined_begin: return session._run_transaction( access_mode, TelemetryAPI.DRIVER, - _work, (query_, parameters, result_transformer_), {} + work, (query_str, parameters, result_transformer_), {} ) @property @@ -1196,7 +1212,7 @@ def _get_server_info(self, session_config) -> ServerInfo: def _work( tx: ManagedTransaction, - query: str, + query: te.LiteralString, parameters: t.Dict[str, t.Any], transformer: t.Callable[[Result], t.Union[_T]] ) -> _T: diff --git a/src/neo4j/_work/query.py b/src/neo4j/_work/query.py index b54df2284..79aedcac8 100644 --- a/src/neo4j/_work/query.py +++ b/src/neo4j/_work/query.py @@ -31,8 +31,10 @@ class Query: """A query with attached extra data. This wrapper class for queries is used to attach extra data to queries - passed to :meth:`.Session.run` and :meth:`.AsyncSession.run`, fulfilling - a similar role as :func:`.unit_of_work` for transactions functions. + passed to :meth:`.Session.run`/:meth:`.AsyncSession.run` and + :meth:`.Driver.execute_query`/:meth:`.AsyncDriver.execute_query`, + fulfilling a similar role as :func:`.unit_of_work` for transactions + functions. :param text: The query text. :type text: typing.LiteralString @@ -76,7 +78,7 @@ def __init__( self.timeout = timeout def __str__(self) -> te.LiteralString: - return str(self.text) + return t.cast(te.LiteralString, str(self.text)) def unit_of_work( diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index 853d5d7a0..9b1e1e9b8 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -365,6 +365,11 @@ async def ExecuteQuery(backend, data): value = config.get(config_key, None) if value is not None: kwargs[kwargs_key] = value + tx_kwargs = fromtestkit.to_tx_kwargs(config) + if tx_kwargs: + query = neo4j.Query(cypher, **tx_kwargs) + else: + query = cypher bookmark_manager_id = config.get("bookmarkManagerId") if bookmark_manager_id is not None: if bookmark_manager_id == -1: @@ -373,7 +378,7 @@ async def ExecuteQuery(backend, data): bookmark_manager = backend.bookmark_managers[bookmark_manager_id] kwargs["bookmark_manager_"] = bookmark_manager - eager_result = await driver.execute_query(cypher, params, **kwargs) + eager_result = await driver.execute_query(query, params, **kwargs) await backend.send_response("EagerResult", { "keys": eager_result.keys, "records": list(map(totestkit.record, eager_result.records)), diff --git a/testkitbackend/_sync/requests.py b/testkitbackend/_sync/requests.py index 620ca476b..8fa5bcfda 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -365,6 +365,11 @@ def ExecuteQuery(backend, data): value = config.get(config_key, None) if value is not None: kwargs[kwargs_key] = value + tx_kwargs = fromtestkit.to_tx_kwargs(config) + if tx_kwargs: + query = neo4j.Query(cypher, **tx_kwargs) + else: + query = cypher bookmark_manager_id = config.get("bookmarkManagerId") if bookmark_manager_id is not None: if bookmark_manager_id == -1: @@ -373,7 +378,7 @@ def ExecuteQuery(backend, data): bookmark_manager = backend.bookmark_managers[bookmark_manager_id] kwargs["bookmark_manager_"] = bookmark_manager - eager_result = driver.execute_query(cypher, params, **kwargs) + eager_result = driver.execute_query(query, params, **kwargs) backend.send_response("EagerResult", { "keys": eager_result.keys, "records": list(map(totestkit.record, eager_result.records)), diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 432aa68f6..0afdabdea 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -35,6 +35,7 @@ ExperimentalWarning, NotificationDisabledCategory, NotificationMinimumSeverity, + Query, TRUST_ALL_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, TrustAll, @@ -74,6 +75,13 @@ def session_cls_mock(mocker): yield session_cls_mock +@pytest.fixture +def unit_of_work_mock(mocker): + unit_of_work_mock = mocker.patch("neo4j._async.driver.unit_of_work", + autospec=True) + yield unit_of_work_mock + + @pytest.mark.parametrize("protocol", ("bolt://", "bolt+s://", "bolt+ssc://")) @pytest.mark.parametrize("host", ("localhost", "127.0.0.1", "[::1]", "[0:0:0:0:0:0:0:1]")) @@ -627,11 +635,20 @@ async def test_execute_query_work(mocker) -> None: assert res is transformer_mock.return_value -@pytest.mark.parametrize("query", ("foo", "bar", "RETURN 1 AS n")) +@pytest.mark.parametrize("query", ( + "foo", + "bar", + "RETURN 1 AS n", + Query("RETURN 1 AS n"), + Query("RETURN 1 AS n", metadata={"key": "value"}), + Query("RETURN 1 AS n", timeout=1234), + Query("RETURN 1 AS n", metadata={"key": "value"}, timeout=1234), +)) @pytest.mark.parametrize("positional", (True, False)) @mark_async_test async def test_execute_query_query( - query: str, positional: bool, session_cls_mock, mocker + query: te.LiteralString | Query, positional: bool, session_cls_mock, + unit_of_work_mock, mocker ) -> None: driver = AsyncGraphDatabase.driver("bolt://localhost") @@ -646,10 +663,21 @@ async def test_execute_query_query( session_mock.__aenter__.assert_awaited_once() session_mock.__aexit__.assert_awaited_once() session_executor_mock = session_mock._run_transaction - session_executor_mock.assert_awaited_once_with( - WRITE_ACCESS, TelemetryAPI.DRIVER, _work, - (query, mocker.ANY, mocker.ANY), {} - ) + if isinstance(query, Query): + unit_of_work_mock.assert_called_once_with(query.metadata, + query.timeout) + unit_of_work = unit_of_work_mock.return_value + unit_of_work.assert_called_once_with(_work) + session_executor_mock.assert_awaited_once_with( + WRITE_ACCESS, TelemetryAPI.DRIVER, unit_of_work.return_value, + (query.text, mocker.ANY, mocker.ANY), {} + ) + else: + unit_of_work_mock.assert_not_called() + session_executor_mock.assert_awaited_once_with( + WRITE_ACCESS, TelemetryAPI.DRIVER, _work, + (query, mocker.ANY, mocker.ANY), {} + ) assert res is session_executor_mock.return_value diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index 11a11e131..99b31c625 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -34,6 +34,7 @@ Neo4jDriver, NotificationDisabledCategory, NotificationMinimumSeverity, + Query, Result, TRUST_ALL_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, @@ -73,6 +74,13 @@ def session_cls_mock(mocker): yield session_cls_mock +@pytest.fixture +def unit_of_work_mock(mocker): + unit_of_work_mock = mocker.patch("neo4j._sync.driver.unit_of_work", + autospec=True) + yield unit_of_work_mock + + @pytest.mark.parametrize("protocol", ("bolt://", "bolt+s://", "bolt+ssc://")) @pytest.mark.parametrize("host", ("localhost", "127.0.0.1", "[::1]", "[0:0:0:0:0:0:0:1]")) @@ -626,11 +634,20 @@ def test_execute_query_work(mocker) -> None: assert res is transformer_mock.return_value -@pytest.mark.parametrize("query", ("foo", "bar", "RETURN 1 AS n")) +@pytest.mark.parametrize("query", ( + "foo", + "bar", + "RETURN 1 AS n", + Query("RETURN 1 AS n"), + Query("RETURN 1 AS n", metadata={"key": "value"}), + Query("RETURN 1 AS n", timeout=1234), + Query("RETURN 1 AS n", metadata={"key": "value"}, timeout=1234), +)) @pytest.mark.parametrize("positional", (True, False)) @mark_sync_test def test_execute_query_query( - query: str, positional: bool, session_cls_mock, mocker + query: te.LiteralString | Query, positional: bool, session_cls_mock, + unit_of_work_mock, mocker ) -> None: driver = GraphDatabase.driver("bolt://localhost") @@ -645,10 +662,21 @@ def test_execute_query_query( session_mock.__enter__.assert_called_once() session_mock.__exit__.assert_called_once() session_executor_mock = session_mock._run_transaction - session_executor_mock.assert_called_once_with( - WRITE_ACCESS, TelemetryAPI.DRIVER, _work, - (query, mocker.ANY, mocker.ANY), {} - ) + if isinstance(query, Query): + unit_of_work_mock.assert_called_once_with(query.metadata, + query.timeout) + unit_of_work = unit_of_work_mock.return_value + unit_of_work.assert_called_once_with(_work) + session_executor_mock.assert_called_once_with( + WRITE_ACCESS, TelemetryAPI.DRIVER, unit_of_work.return_value, + (query.text, mocker.ANY, mocker.ANY), {} + ) + else: + unit_of_work_mock.assert_not_called() + session_executor_mock.assert_called_once_with( + WRITE_ACCESS, TelemetryAPI.DRIVER, _work, + (query, mocker.ANY, mocker.ANY), {} + ) assert res is session_executor_mock.return_value From 59f40ec09b6b24c90d836cb31331e029b0b43af8 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 22 Nov 2023 13:59:37 +0100 Subject: [PATCH 2/4] Fix using typing_extensions at runtime --- src/neo4j/_work/query.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/neo4j/_work/query.py b/src/neo4j/_work/query.py index 79aedcac8..fc2b7c422 100644 --- a/src/neo4j/_work/query.py +++ b/src/neo4j/_work/query.py @@ -78,7 +78,12 @@ def __init__( self.timeout = timeout def __str__(self) -> te.LiteralString: - return t.cast(te.LiteralString, str(self.text)) + # we know that if Query is constructed with a LiteralString, + # str(self.text) will be a LiteralString as well. The conversion isn't + # necessary if the user adheres to the type hints. However, it was + # here before, and we don't want to break backwards compatibility. + text: te.LiteralString = str(self.text) # type: ignore[assignment] + return text def unit_of_work( From 887101fa267c191b4464f49502405f3182a6e172 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 22 Nov 2023 14:54:56 +0100 Subject: [PATCH 3/4] Docs: revert type to equivalent original --- docs/source/api.rst | 2 +- docs/source/async_api.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index b3da41940..7a496bf00 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -252,7 +252,7 @@ Closing a driver will immediately shut down all connections in the pool. transaction configuration. :type query_: typing.LiteralString | Query :param parameters_: parameters to use in the query - :type parameters_: typing.Optional[typing.Dict[str, typing.Any]] + :type parameters_: typing.Dict[str, typing.Any] | None :param routing_: Whether to route the query to a reader (follower/read replica) or a writer (leader) in the cluster. Default is to route to a writer. diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 6f1c793cb..5d1585ea6 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -239,7 +239,7 @@ Closing a driver will immediately shut down all connections in the pool. transaction configuration. :type query_: typing.LiteralString | Query :param parameters_: parameters to use in the query - :type parameters_: typing.Optional[typing.Dict[str, typing.Any]] + :type parameters_: typing.Dict[str, typing.Any] | None :param routing_: Whether to route the query to a reader (follower/read replica) or a writer (leader) in the cluster. Default is to route to a writer. From dd2f644ffa80f5d1dd8b343e5cc0dd607afe5157 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 23 Nov 2023 10:50:15 +0100 Subject: [PATCH 4/4] API docs: mention version of this change --- docs/source/api.rst | 4 ++++ docs/source/async_api.rst | 4 ++++ src/neo4j/_async/driver.py | 4 ++++ src/neo4j/_sync/driver.py | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/docs/source/api.rst b/docs/source/api.rst index 7a496bf00..7a30f6004 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -379,6 +379,10 @@ Closing a driver will immediately shut down all connections in the pool. .. versionchanged:: 5.14 Stabilized ``auth_`` parameter from preview. + .. versionchanged:: 5.15 + The ``query_`` parameter now also accepts a :class:`.Query` object + instead of only :class:`str`. + .. _driver-configuration-ref: diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 5d1585ea6..a346b538a 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -366,6 +366,10 @@ Closing a driver will immediately shut down all connections in the pool. .. versionchanged:: 5.14 Stabilized ``auth_`` parameter from preview. + .. versionchanged:: 5.15 + The ``query_`` parameter now also accepts a :class:`.Query` object + instead of only :class:`str`. + .. _async-driver-configuration-ref: diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index e201ff580..8bbbb9d2f 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -846,6 +846,10 @@ async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: .. versionchanged:: 5.14 Stabilized ``auth_`` parameter from preview. + + .. versionchanged:: 5.15 + The ``query_`` parameter now also accepts a :class:`.Query` object + instead of only :class:`str`. """ self._check_state() invalid_kwargs = [k for k in kwargs if diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index c0ef5faf2..0bed94284 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -845,6 +845,10 @@ def example(driver: neo4j.Driver) -> neo4j.Record:: .. versionchanged:: 5.14 Stabilized ``auth_`` parameter from preview. + + .. versionchanged:: 5.15 + The ``query_`` parameter now also accepts a :class:`.Query` object + instead of only :class:`str`. """ self._check_state() invalid_kwargs = [k for k in kwargs if