From 44e0dd06d79857bd453cf77bcd3ba68bc55c2bf2 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 17 Oct 2022 15:48:53 +0200 Subject: [PATCH 01/15] Introduce driver.execute_query --- docs/source/api.rst | 167 ++++++++++++++- docs/source/async_api.rst | 150 ++++++++++++- neo4j/__init__.py | 4 + neo4j/_api.py | 53 +++++ neo4j/_async/driver.py | 295 +++++++++++++++++++++++++- neo4j/_async/work/__init__.py | 7 +- neo4j/_async/work/result.py | 34 ++- neo4j/_sync/driver.py | 295 +++++++++++++++++++++++++- neo4j/_sync/work/__init__.py | 7 +- neo4j/_sync/work/result.py | 34 ++- neo4j/_work/__init__.py | 24 +++ neo4j/_work/_eager_result.py | 50 +++++ testkitbackend/_async/requests.py | 66 +++--- testkitbackend/_sync/requests.py | 66 +++--- testkitbackend/test_config.json | 1 + testkitbackend/totestkit.py | 40 ++++ tests/unit/async_/test_driver.py | 272 ++++++++++++++++++++++++ tests/unit/async_/work/test_result.py | 48 ++++- tests/unit/sync/test_driver.py | 272 ++++++++++++++++++++++++ tests/unit/sync/work/test_result.py | 48 ++++- 20 files changed, 1827 insertions(+), 106 deletions(-) create mode 100644 neo4j/_api.py create mode 100644 neo4j/_work/__init__.py create mode 100644 neo4j/_work/_eager_result.py diff --git a/docs/source/api.rst b/docs/source/api.rst index 3de8da154..a24923975 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -151,7 +151,152 @@ Closing a driver will immediately shut down all connections in the pool. query, use :meth:`neo4j.Driver.verify_connectivity`. .. autoclass:: neo4j.Driver() - :members: session, encrypted, close, verify_connectivity, get_server_info + :members: session, query_bookmark_manager, encrypted, close, + verify_connectivity, get_server_info + + .. method:: execute_query(query, parameters=None,routing=neo4j.RoutingControl.WRITERS, database=None, impersonated_user=None, bookmark_manager=self.query_bookmark_manager, result_transformer=Result.to_eager_result, **kwargs) + + Execute a query inside a retired transaction and return all results. + + This method is a handy wrapper for lower-level driver APIs like + sessions, transactions, and transaction functions. It is intended + for simple use cases where there is no need for managing all possible + options. + + The method is roughly equivalent to:: + + def execute_query( + query, parameters, routing, database, impersonated_user, + bookmark_manager, result_transformer, + ): + def work(tx): + result = tx.run(query, parameters) + return some_transformer(result) + + with driver.session( + database=database, + impersonated_user=impersonated_user, + bookmark_manager=bookmark_manager, + ) as session: + if routing == RoutingControl.WRITERS: + return session.execute_write(work) + elif routing == RoutingControl.READERS: + return session.execute_read(work) + + Usage example:: + + from typing import List + + import neo4j + + def example(driver: neo4j.Driver) -> List[str]: + """Get the name of all 42 year-olds.""" + records, summary, keys = driver.execute_query( + "MATCH (p:Person {age: $age}) RETURN p.name", + {"age": 42}, + routing=neo4j.RoutingControl.READERS, # or just "r" + database="neo4j", + ) + assert keys == ["p.name"] # not needed, just for illustration + log.debug("some meta data: %s", summary) + return [str(record["p.name"]) for record in records] + # or: return [str(record[0]) for record in records] + # or even: return list(map(lambda r: str(r[0]), records)) + + Another example:: + + import neo4j + + def example(driver: neo4j.Driver) -> int: + """Call all young people "My dear" and get their count.""" + record = driver.execute_query( + "MATCH (p:Person) WHERE n.age <= 15 " + "SET p.nickname = 'My dear' " + "RETURN count(*)", + routing=neo4j.RoutingControl.WRITERS, # or just "w" + database="neo4j", + result_transformer=neo4j.Result.single, + ) + count = record[0] + assert isinstance(count, int) + return count + + :param query: cypher query to execute + :type query: typing.Optional[str] + :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 + a writer (leader) in the cluster. Default is to route to a writer. + :type routing: neo4j.RoutingControl + :param database: + database to execute the query against. + + None (default) uses the database configured on the server side. + + .. Note:: + It is recommended to always specify the database explicitly + when possible. This allows the driver to work more efficiently, + as it will not have to resolve the default database first. + + See also the Session config :ref:`database-ref`. + :type database: typing.Optional[str] + :param impersonated_user: + Name of the user to impersonate. + + This means that all query will be executed in the security context + of the impersonated user. For this, the user for which the + :class:`Driver` has been created needs to have the appropriate + permissions. + + See also the Session config + :type impersonated_user: typing.Optional[str] + :param result_transformer: + A function that gets passed the :class:`neo4j.Result` object + resulting from the query and converts it to a different type. The + result of the transformer function is returned by this method. + + .. warning:: + + The transformer function must **not** return the + :class:`neo4j.Result` itself. + + Example transformer that checks that exactly one record is in the + result stream, then returns the record and the result summary:: + + from typing import Tuple + + import neo4j + + def transformer( + result: neo4j.Result + ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: + record = result.single(strict=True) + summary = result.consume() + return record, summary + + :type result_transformer: + typing.Callable[[neo4j.Result], typing.Union[T]] + :param bookmark_manager: + Specify a bookmark manager to use. + + If present, the bookmark manager is used to keep the query causally + consistent with all work executed using the same bookmark manager. + + Defaults to the driver's :attr:`.query_bookmark_manager`. + + Pass :const:`None` to disable causal consistency. + :type bookmark_manager: + typing.Union[neo4j.BookmarkManager, neo4j.BookmarkManager, + None] + :param kwargs: additional keyword parameters. + These take precedence over parameters passed as ``parameters``. + :type kwargs: typing.Any + + :returns: the result of the ``result_transformer`` + :rtype: T + + .. versionadded:: 5.2 .. _driver-configuration-ref: @@ -921,11 +1066,22 @@ A :class:`neo4j.Result` is attached to an active connection, through a :class:`n .. automethod:: to_df + .. automethod:: to_eager_result + .. automethod:: closed See https://neo4j.com/docs/python-manual/current/cypher-workflow/#python-driver-type-mapping for more about type mapping. +*********** +EagerResult +*********** + +.. autoclass:: neo4j.EagerResult + :show-inheritance: + :members: + + Graph ===== @@ -1265,6 +1421,15 @@ BookmarkManager :members: +************************* +Constants, Enums, Helpers +************************* + +.. autoclass:: neo4j.RoutingControl + :show-inheritance: + :members: + + .. _errors-ref: ****** diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index c39e7b77b..4f76373d8 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -134,7 +134,153 @@ Closing a driver will immediately shut down all connections in the pool. query, use :meth:`neo4j.AsyncDriver.verify_connectivity`. .. autoclass:: neo4j.AsyncDriver() - :members: session, encrypted, close, verify_connectivity, get_server_info + :members: session, query_bookmark_manager, encrypted, close, + verify_connectivity, get_server_info + + .. method:: execute_query(query, parameters=None,routing=neo4j.RoutingControl.WRITERS, database=None, impersonated_user=None, bookmark_manager=self.query_bookmark_manager, result_transformer=AsyncResult.to_eager_result, **kwargs) + :async: + + Execute a query inside a retired transaction and return all results. + + This method is a handy wrapper for lower-level driver APIs like + sessions, transactions, and transaction functions. It is intended + for simple use cases where there is no need for managing all possible + options. + + The method is roughly equivalent to:: + + async def execute_query( + query, parameters, routing, database, impersonated_user, + bookmark_manager, result_transformer, + ): + async def work(tx): + result = await tx.run(query, parameters) + return await some_transformer(result) + + async with driver.session( + database=database, + impersonated_user=impersonated_user, + bookmark_manager=bookmark_manager, + ) as session: + if routing == RoutingControl.WRITERS: + return await session.execute_write(work) + elif routing == RoutingControl.READERS: + return await session.execute_read(work) + + Usage example:: + + from typing import List + + import neo4j + + async def example(driver: neo4j.AsyncDriver) -> List[str]: + """Get the name of all 42 year-olds.""" + records, summary, keys = await driver.execute_query( + "MATCH (p:Person {age: $age}) RETURN p.name", + {"age": 42}, + routing=neo4j.RoutingControl.READERS, # or just "r" + database="neo4j", + ) + assert keys == ["p.name"] # not needed, just for illustration + log.debug("some meta data: %s", summary) + return [str(record["p.name"]) for record in records] + # or: return [str(record[0]) for record in records] + # or even: return list(map(lambda r: str(r[0]), records)) + + Another example:: + + import neo4j + + async def example(driver: neo4j.AsyncDriver) -> int: + """Call all young people "My dear" and get their count.""" + record = await driver.execute_query( + "MATCH (p:Person) WHERE n.age <= 15 " + "SET p.nickname = 'My dear' " + "RETURN count(*)", + routing=neo4j.RoutingControl.WRITERS, # or just "w" + database="neo4j", + result_transformer=neo4j.AsyncResult.single, + ) + count = record[0] + assert isinstance(count, int) + return count + + :param query: cypher query to execute + :type query: typing.Optional[str] + :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 + a writer (leader) in the cluster. Default is to route to a writer. + :type routing: neo4j.RoutingControl + :param database: + database to execute the query against. + + None (default) uses the database configured on the server side. + + .. Note:: + It is recommended to always specify the database explicitly + when possible. This allows the driver to work more efficiently, + as it will not have to resolve the default database first. + + See also the Session config :ref:`database-ref`. + :type database: typing.Optional[str] + :param impersonated_user: + Name of the user to impersonate. + + This means that all query will be executed in the security context + of the impersonated user. For this, the user for which the + :class:`Driver` has been created needs to have the appropriate + permissions. + + See also the Session config + :type impersonated_user: typing.Optional[str] + :param result_transformer: + A function that gets passed the :class:`neo4j.AsyncResult` object + resulting from the query and converts it to a different type. The + result of the transformer function is returned by this method. + + .. warning:: + + The transformer function must **not** return the + :class:`neo4j.AsyncResult` itself. + + Example transformer that checks that exactly one record is in the + result stream, then returns the record and the result summary:: + + from typing import Tuple + + import neo4j + + async def transformer( + result: neo4j.AsyncResult + ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: + record = await result.single(strict=True) + summary = await result.consume() + return record, summary + + :type result_transformer: + typing.Callable[[neo4j.AsyncResult], typing.Awaitable[T]] + :param bookmark_manager: + Specify a bookmark manager to use. + + If present, the bookmark manager is used to keep the query causally + consistent with all work executed using the same bookmark manager. + + Defaults to the driver's :attr:`.query_bookmark_manager`. + + Pass :const:`None` to disable causal consistency. + :type bookmark_manager: + typing.Union[neo4j.AsyncBookmarkManager, neo4j.BookmarkManager, + None] + :param kwargs: additional keyword parameters. + These take precedence over parameters passed as ``parameters``. + :type kwargs: typing.Any + + :returns: the result of the ``result_transformer`` + :rtype: T + + .. versionadded:: 5.2 .. _async-driver-configuration-ref: @@ -593,6 +739,8 @@ A :class:`neo4j.AsyncResult` is attached to an active connection, through a :cla .. automethod:: to_df + .. automethod:: to_eager_result + .. automethod:: closed See https://neo4j.com/docs/python-manual/current/cypher-workflow/#python-driver-type-mapping for more about type mapping. diff --git a/neo4j/__init__.py b/neo4j/__init__.py index df8590bee..dd21781d9 100644 --- a/neo4j/__init__.py +++ b/neo4j/__init__.py @@ -18,6 +18,7 @@ from logging import getLogger as _getLogger +from ._api import RoutingControl from ._async.driver import ( AsyncBoltDriver, AsyncDriver, @@ -57,6 +58,7 @@ Session, Transaction, ) +from ._work import EagerResult from .addressing import ( Address, IPv4Address, @@ -112,6 +114,7 @@ "custom_auth", "DEFAULT_DATABASE", "Driver", + "EagerResult", "ExperimentalWarning", "get_user_agent", "GraphDatabase", @@ -127,6 +130,7 @@ "Record", "Result", "ResultSummary", + "RoutingControl", "ServerInfo", "Session", "SessionConfig", diff --git a/neo4j/_api.py b/neo4j/_api.py new file mode 100644 index 000000000..8c472c23a --- /dev/null +++ b/neo4j/_api.py @@ -0,0 +1,53 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://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 +# +# https://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. + + +from __future__ import annotations + +import typing as t +from enum import Enum + + +if t.TYPE_CHECKING: + import typing_extensions as te + + +class RoutingControl(str, Enum): + """Selection which cluster members to route a query connect to. + + Inherits from :class:`str` and :class:`Enum`. Hence, every driver API + accepting a :class:`.RoutingControl` value will also accept a string + + >>> RoutingControl.READERS == "r" + True + >>> RoutingControl.WRITERS == "w" + True + + .. seealso:: + :attr:`.AsyncDriver.execute_query`, :attr:`.Driver.execute_query` + + .. versionadded:: 5.2 + """ + READERS = "r" + WRITERS = "w" + + +if t.TYPE_CHECKING: + T_RoutingControl = t.Union[ + RoutingControl, + te.Literal["r", "w"], + ] diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index 37062581b..47fab9f21 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -19,13 +19,9 @@ from __future__ import annotations import typing as t +import warnings - -if t.TYPE_CHECKING: - import typing_extensions as te - - import ssl - +from .._api import RoutingControl from .._async_compat.util import AsyncUtil from .._conf import ( Config, @@ -39,8 +35,10 @@ deprecation_warn, experimental, experimental_warn, + ExperimentalWarning, unclosed_resource_warn, ) +from .._work import EagerResult from ..addressing import Address from ..api import ( AsyncBookmarkManager, @@ -69,7 +67,31 @@ T_BmConsumer as _T_BmConsumer, T_BmSupplier as _T_BmSupplier, ) -from .work import AsyncSession +from .work import ( + AsyncManagedTransaction, + AsyncResult, + AsyncSession, +) + + +if t.TYPE_CHECKING: + import ssl + from enum import Enum + + import typing_extensions as te + + from .._api import T_RoutingControl + + + class _DefaultEnum(Enum): + default = "default" + + _default = _DefaultEnum.default + +else: + _default = object() + +_T = t.TypeVar("_T") class AsyncGraphDatabase: @@ -389,6 +411,12 @@ def __init__(self, pool, default_workspace_config): assert default_workspace_config is not None self._pool = pool self._default_workspace_config = default_workspace_config + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + message=r".*\bbookmark manager\b.*", + category=ExperimentalWarning) + self._query_bookmark_manager = \ + AsyncGraphDatabase.bookmark_manager() async def __aenter__(self) -> AsyncDriver: return self @@ -457,6 +485,249 @@ async def close(self) -> None: await self._pool.close() self._closed = True + # overloads to work around https://github.com/python/mypy/issues/3737 + @t.overload + async def execute_query( + self, + query: str, + parameters: t.Dict[str, t.Any] = None, + routing: T_RoutingControl = RoutingControl.WRITERS, + database: str = None, + impersonated_user: str = None, + bookmark_manager: t.Union[ + AsyncBookmarkManager, BookmarkManager, None + ] = ..., + result_transformer: t.Callable[ + [AsyncResult], t.Awaitable[EagerResult] + ] = ..., + **kwargs: t.Any + ) -> EagerResult: + ... + + @t.overload + async def execute_query( + self, + query: str, + parameters: t.Dict[str, t.Any] = None, + routing: T_RoutingControl = RoutingControl.WRITERS, + database: str = None, + impersonated_user: str = None, + bookmark_manager: t.Union[ + AsyncBookmarkManager, BookmarkManager, None + ] = ..., + result_transformer: t.Callable[ + [AsyncResult], t.Awaitable[_T] + ] = ..., + **kwargs: t.Any + ) -> _T: + ... + + async def execute_query( + self, + query: str, + parameters: t.Dict[str, t.Any] = None, + routing: T_RoutingControl = RoutingControl.WRITERS, + database: str = None, + impersonated_user: str = None, + bookmark_manager: t.Union[ + AsyncBookmarkManager, BookmarkManager, None, + te.Literal[_DefaultEnum.default] + ] = _default, + result_transformer: t.Callable[[AsyncResult], t.Awaitable[_T]] = ( + # cast to work around https://github.com/python/mypy/issues/3737 + t.cast(t.Callable[[AsyncResult], t.Awaitable[_T]], + AsyncResult.to_eager_result) + ), + **kwargs: t.Any + ) -> _T: + """Execute a query inside a retired transaction and return all results. + + This method is a handy wrapper for lower-level driver APIs like + sessions, transactions, and transaction functions. It is intended + for simple use cases where there is no need for managing all possible + options. + + The method is roughly equivalent to:: + + async def execute_query( + query, parameters, routing, database, impersonated_user, + bookmark_manager, result_transformer, + ): + async def work(tx): + result = await tx.run(query, parameters) + return await some_transformer(result) + + async with driver.session( + database=database, + impersonated_user=impersonated_user, + bookmark_manager=bookmark_manager, + ) as session: + if routing == RoutingControl.WRITERS: + return await session.execute_write(work) + elif routing == RoutingControl.READERS: + return await session.execute_read(work) + + Usage example:: + + from typing import List + + import neo4j + + async def example(driver: neo4j.AsyncDriver) -> List[str]: + \"""Get the name of all 42 year-olds.\""" + records, summary, keys = await driver.execute_query( + "MATCH (p:Person {age: $age}) RETURN p.name", + {"age": 42}, + routing=neo4j.RoutingControl.READERS, # or just "r" + database="neo4j", + ) + assert keys == ["p.name"] # not needed, just for illustration + log.debug("some meta data: %s", summary) + return [str(record["p.name"]) for record in records] + # or: return [str(record[0]) for record in records] + # or even: return list(map(lambda r: str(r[0]), records)) + + Another example:: + + import neo4j + + async def example(driver: neo4j.AsyncDriver) -> int: + \"""Call all young people "My dear" and get their count.\""" + record = await driver.execute_query( + "MATCH (p:Person) WHERE n.age <= 15 " + "SET p.nickname = 'My dear' " + "RETURN count(*)", + routing=neo4j.RoutingControl.WRITERS, # or just "w" + database="neo4j", + result_transformer=neo4j.AsyncResult.single, + ) + count = record[0] + assert isinstance(count, int) + return count + + :param query: cypher query to execute + :type query: typing.Optional[str] + :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 + a writer (leader) in the cluster. Default is to route to a writer. + :type routing: neo4j.RoutingControl + :param database: + database to execute the query against. + + None (default) uses the database configured on the server side. + + .. Note:: + It is recommended to always specify the database explicitly + when possible. This allows the driver to work more efficiently, + as it will not have to resolve the default database first. + + See also the Session config :ref:`database-ref`. + :type database: typing.Optional[str] + :param impersonated_user: + Name of the user to impersonate. + + This means that all query will be executed in the security context + of the impersonated user. For this, the user for which the + :class:`Driver` has been created needs to have the appropriate + permissions. + + See also the Session config + :type impersonated_user: typing.Optional[str] + :param result_transformer: + A function that gets passed the :class:`neo4j.AsyncResult` object + resulting from the query and converts it to a different type. The + result of the transformer function is returned by this method. + + .. warning:: + + The transformer function must **not** return the + :class:`neo4j.AsyncResult` itself. + + Example transformer that checks that exactly one record is in the + result stream, then returns the record and the result summary:: + + from typing import Tuple + + import neo4j + + async def transformer( + result: neo4j.AsyncResult + ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: + record = await result.single(strict=True) + summary = await result.consume() + return record, summary + + :type result_transformer: + typing.Callable[[neo4j.AsyncResult], typing.Awaitable[T]] + :param bookmark_manager: + Specify a bookmark manager to use. + + If present, the bookmark manager is used to keep the query causally + consistent with all work executed using the same bookmark manager. + + Defaults to the driver's :attr:`.query_bookmark_manager`. + + Pass :const:`None` to disable causal consistency. + :type bookmark_manager: + typing.Union[neo4j.AsyncBookmarkManager, neo4j.BookmarkManager, + None] + :param kwargs: additional keyword parameters. + These take precedence over parameters passed as ``parameters``. + :type kwargs: typing.Any + + :returns: the result of the ``result_transformer`` + :rtype: T + + .. versionadded:: 5.2 + """ + parameters = dict(parameters or {}, **kwargs) + + if bookmark_manager is _default: + bookmark_manager = self._query_bookmark_manager + assert bookmark_manager is not _default + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + message=r".*\bbookmark_manager\b.*", + category=ExperimentalWarning) + session = self.session(database=database, + impersonated_user=impersonated_user, + bookmark_manager=bookmark_manager) + async with session: + # TODO: test all examples in the docstring + + if routing == RoutingControl.WRITERS: + executor = session.execute_write + elif routing == RoutingControl.READERS: + executor = session.execute_read + else: + raise ValueError("Invalid routing control value: %r" % routing) + return await executor(_work, query, parameters, result_transformer) + + @property + def query_bookmark_manager(self) -> AsyncBookmarkManager: + """The driver's default query bookmark manager. + + This is the default :class:`AsyncBookmarkManager` used by + :meth:`.execute_query`. This can be used to causally chain + :meth:`.execute_query` calls and sessions. Example:: + + async def example(driver: neo4j.AsyncDriver) -> None: + await driver.execute_query("") + async with driver.session( + bookmark_manager=driver.query_bookmark_manager + ) as session: + # every query inside this session will be causally chained + # (i.e., can read what was written by ) + await session.run("") + # subsequent execute_query calls will be causally chained + # (i.e., can read what was written by ) + await driver.execute_query("") + """ + return self._query_bookmark_manager + if t.TYPE_CHECKING: async def verify_connectivity( @@ -601,6 +872,16 @@ async def supports_multi_db(self) -> bool: return session._connection.supports_multiple_databases +async def _work( + tx: AsyncManagedTransaction, + query: str, + parameters: t.Dict[str, t.Any], + transformer: t.Callable[[AsyncResult], t.Awaitable[_T]] +) -> _T: + res = await tx.run(query, parameters) + return await transformer(res) + + class AsyncBoltDriver(_Direct, AsyncDriver): """:class:`.AsyncBoltDriver` is instantiated for ``bolt`` URIs and addresses a single database machine. This may be a standalone server or diff --git a/neo4j/_async/work/__init__.py b/neo4j/_async/work/__init__.py index 264e6cb99..e769bdd85 100644 --- a/neo4j/_async/work/__init__.py +++ b/neo4j/_async/work/__init__.py @@ -15,9 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from .session import ( +from .result import ( AsyncResult, + EagerResult, +) +from .session import ( AsyncSession, AsyncWorkspace, ) @@ -35,4 +37,5 @@ "AsyncTransaction", "AsyncTransactionBase", "AsyncWorkspace", + "EagerResult", ] diff --git a/neo4j/_async/work/result.py b/neo4j/_async/work/result.py index ffe901fee..14278cfa4 100644 --- a/neo4j/_async/work/result.py +++ b/neo4j/_async/work/result.py @@ -19,7 +19,10 @@ from __future__ import annotations import typing as t -from collections import deque +from collections import ( + deque, + namedtuple, +) from warnings import warn @@ -33,6 +36,7 @@ RecordTableRowExporter, ) from ..._meta import experimental +from ..._work import EagerResult from ...exceptions import ( ResultConsumedError, ResultNotSingleError, @@ -581,6 +585,28 @@ async def data(self, *keys: _T_ResultKey) -> t.List[t.Any]: """ return [record.data(*keys) async for record in self] + async def to_eager_result(self) -> EagerResult: + """Convert this result to an :class:`.EagerResult`. + + This method exhausts the result and triggers a :meth:`.consume`. + + :returns: all remaining records in the result stream, the result's + summary, and keys as an :class:`.EagerResult` instance. + + :raises ResultConsumedError: if the transaction from which this result + was obtained has been closed or the Result has been explicitly + consumed. + + .. versionadded:: 5.2 + """ + + await self._buffer_all() + return EagerResult( + keys=list(self.keys()), + records=await AsyncUtil.list(self), + summary=await self.consume() + ) + async def to_df( self, expand: bool = False, @@ -711,9 +737,9 @@ async def to_df( df[dt_columns] = df[dt_columns].apply( lambda col: col.map( lambda x: - pd.Timestamp(x.iso_format()) - .replace(tzinfo=getattr(x, "tzinfo", None)) - if x else pd.NaT + pd.Timestamp(x.iso_format()) + .replace(tzinfo=getattr(x, "tzinfo", None)) + if x else pd.NaT ) ) return df diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index 18d2c8de5..5fd47d0f7 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -19,13 +19,9 @@ from __future__ import annotations import typing as t +import warnings - -if t.TYPE_CHECKING: - import typing_extensions as te - - import ssl - +from .._api import RoutingControl from .._async_compat.util import Util from .._conf import ( Config, @@ -39,8 +35,10 @@ deprecation_warn, experimental, experimental_warn, + ExperimentalWarning, unclosed_resource_warn, ) +from .._work import EagerResult from ..addressing import Address from ..api import ( Auth, @@ -68,7 +66,31 @@ T_BmConsumer as _T_BmConsumer, T_BmSupplier as _T_BmSupplier, ) -from .work import Session +from .work import ( + ManagedTransaction, + Result, + Session, +) + + +if t.TYPE_CHECKING: + import ssl + from enum import Enum + + import typing_extensions as te + + from .._api import T_RoutingControl + + + class _DefaultEnum(Enum): + default = "default" + + _default = _DefaultEnum.default + +else: + _default = object() + +_T = t.TypeVar("_T") class GraphDatabase: @@ -388,6 +410,12 @@ def __init__(self, pool, default_workspace_config): assert default_workspace_config is not None self._pool = pool self._default_workspace_config = default_workspace_config + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + message=r".*\bbookmark manager\b.*", + category=ExperimentalWarning) + self._query_bookmark_manager = \ + GraphDatabase.bookmark_manager() def __enter__(self) -> Driver: return self @@ -456,6 +484,249 @@ def close(self) -> None: self._pool.close() self._closed = True + # overloads to work around https://github.com/python/mypy/issues/3737 + @t.overload + def execute_query( + self, + query: str, + parameters: t.Dict[str, t.Any] = None, + routing: T_RoutingControl = RoutingControl.WRITERS, + database: str = None, + impersonated_user: str = None, + bookmark_manager: t.Union[ + BookmarkManager, BookmarkManager, None + ] = ..., + result_transformer: t.Callable[ + [Result], t.Union[EagerResult] + ] = ..., + **kwargs: t.Any + ) -> EagerResult: + ... + + @t.overload + def execute_query( + self, + query: str, + parameters: t.Dict[str, t.Any] = None, + routing: T_RoutingControl = RoutingControl.WRITERS, + database: str = None, + impersonated_user: str = None, + bookmark_manager: t.Union[ + BookmarkManager, BookmarkManager, None + ] = ..., + result_transformer: t.Callable[ + [Result], t.Union[_T] + ] = ..., + **kwargs: t.Any + ) -> _T: + ... + + def execute_query( + self, + query: str, + parameters: t.Dict[str, t.Any] = None, + routing: T_RoutingControl = RoutingControl.WRITERS, + database: str = None, + impersonated_user: str = None, + bookmark_manager: t.Union[ + BookmarkManager, BookmarkManager, None, + te.Literal[_DefaultEnum.default] + ] = _default, + result_transformer: t.Callable[[Result], t.Union[_T]] = ( + # cast to work around https://github.com/python/mypy/issues/3737 + t.cast(t.Callable[[Result], t.Union[_T]], + Result.to_eager_result) + ), + **kwargs: t.Any + ) -> _T: + """Execute a query inside a retired transaction and return all results. + + This method is a handy wrapper for lower-level driver APIs like + sessions, transactions, and transaction functions. It is intended + for simple use cases where there is no need for managing all possible + options. + + The method is roughly equivalent to:: + + def execute_query( + query, parameters, routing, database, impersonated_user, + bookmark_manager, result_transformer, + ): + def work(tx): + result = tx.run(query, parameters) + return some_transformer(result) + + with driver.session( + database=database, + impersonated_user=impersonated_user, + bookmark_manager=bookmark_manager, + ) as session: + if routing == RoutingControl.WRITERS: + return session.execute_write(work) + elif routing == RoutingControl.READERS: + return session.execute_read(work) + + Usage example:: + + from typing import List + + import neo4j + + def example(driver: neo4j.Driver) -> List[str]: + \"""Get the name of all 42 year-olds.\""" + records, summary, keys = driver.execute_query( + "MATCH (p:Person {age: $age}) RETURN p.name", + {"age": 42}, + routing=neo4j.RoutingControl.READERS, # or just "r" + database="neo4j", + ) + assert keys == ["p.name"] # not needed, just for illustration + log.debug("some meta data: %s", summary) + return [str(record["p.name"]) for record in records] + # or: return [str(record[0]) for record in records] + # or even: return list(map(lambda r: str(r[0]), records)) + + Another example:: + + import neo4j + + def example(driver: neo4j.Driver) -> int: + \"""Call all young people "My dear" and get their count.\""" + record = driver.execute_query( + "MATCH (p:Person) WHERE n.age <= 15 " + "SET p.nickname = 'My dear' " + "RETURN count(*)", + routing=neo4j.RoutingControl.WRITERS, # or just "w" + database="neo4j", + result_transformer=neo4j.Result.single, + ) + count = record[0] + assert isinstance(count, int) + return count + + :param query: cypher query to execute + :type query: typing.Optional[str] + :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 + a writer (leader) in the cluster. Default is to route to a writer. + :type routing: neo4j.RoutingControl + :param database: + database to execute the query against. + + None (default) uses the database configured on the server side. + + .. Note:: + It is recommended to always specify the database explicitly + when possible. This allows the driver to work more efficiently, + as it will not have to resolve the default database first. + + See also the Session config :ref:`database-ref`. + :type database: typing.Optional[str] + :param impersonated_user: + Name of the user to impersonate. + + This means that all query will be executed in the security context + of the impersonated user. For this, the user for which the + :class:`Driver` has been created needs to have the appropriate + permissions. + + See also the Session config + :type impersonated_user: typing.Optional[str] + :param result_transformer: + A function that gets passed the :class:`neo4j.Result` object + resulting from the query and converts it to a different type. The + result of the transformer function is returned by this method. + + .. warning:: + + The transformer function must **not** return the + :class:`neo4j.Result` itself. + + Example transformer that checks that exactly one record is in the + result stream, then returns the record and the result summary:: + + from typing import Tuple + + import neo4j + + def transformer( + result: neo4j.Result + ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: + record = result.single(strict=True) + summary = result.consume() + return record, summary + + :type result_transformer: + typing.Callable[[neo4j.Result], typing.Union[T]] + :param bookmark_manager: + Specify a bookmark manager to use. + + If present, the bookmark manager is used to keep the query causally + consistent with all work executed using the same bookmark manager. + + Defaults to the driver's :attr:`.query_bookmark_manager`. + + Pass :const:`None` to disable causal consistency. + :type bookmark_manager: + typing.Union[neo4j.BookmarkManager, neo4j.BookmarkManager, + None] + :param kwargs: additional keyword parameters. + These take precedence over parameters passed as ``parameters``. + :type kwargs: typing.Any + + :returns: the result of the ``result_transformer`` + :rtype: T + + .. versionadded:: 5.2 + """ + parameters = dict(parameters or {}, **kwargs) + + if bookmark_manager is _default: + bookmark_manager = self._query_bookmark_manager + assert bookmark_manager is not _default + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + message=r".*\bbookmark_manager\b.*", + category=ExperimentalWarning) + session = self.session(database=database, + impersonated_user=impersonated_user, + bookmark_manager=bookmark_manager) + with session: + # TODO: test all examples in the docstring + + if routing == RoutingControl.WRITERS: + executor = session.execute_write + elif routing == RoutingControl.READERS: + executor = session.execute_read + else: + raise ValueError("Invalid routing control value: %r" % routing) + return executor(_work, query, parameters, result_transformer) + + @property + def query_bookmark_manager(self) -> BookmarkManager: + """The driver's default query bookmark manager. + + This is the default :class:`BookmarkManager` used by + :meth:`.execute_query`. This can be used to causally chain + :meth:`.execute_query` calls and sessions. Example:: + + def example(driver: neo4j.Driver) -> None: + driver.execute_query("") + with driver.session( + bookmark_manager=driver.query_bookmark_manager + ) as session: + # every query inside this session will be causally chained + # (i.e., can read what was written by ) + session.run("") + # subsequent execute_query calls will be causally chained + # (i.e., can read what was written by ) + driver.execute_query("") + """ + return self._query_bookmark_manager + if t.TYPE_CHECKING: def verify_connectivity( @@ -600,6 +871,16 @@ def supports_multi_db(self) -> bool: return session._connection.supports_multiple_databases +def _work( + tx: ManagedTransaction, + query: str, + parameters: t.Dict[str, t.Any], + transformer: t.Callable[[Result], t.Union[_T]] +) -> _T: + res = tx.run(query, parameters) + return transformer(res) + + class BoltDriver(_Direct, Driver): """:class:`.BoltDriver` is instantiated for ``bolt`` URIs and addresses a single database machine. This may be a standalone server or diff --git a/neo4j/_sync/work/__init__.py b/neo4j/_sync/work/__init__.py index 92a1af0d4..e5f1ddb1c 100644 --- a/neo4j/_sync/work/__init__.py +++ b/neo4j/_sync/work/__init__.py @@ -15,9 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from .session import ( +from .result import ( + EagerResult, Result, +) +from .session import ( Session, Workspace, ) @@ -35,4 +37,5 @@ "Transaction", "TransactionBase", "Workspace", + "EagerResult", ] diff --git a/neo4j/_sync/work/result.py b/neo4j/_sync/work/result.py index de7a7deb5..fa788e3ce 100644 --- a/neo4j/_sync/work/result.py +++ b/neo4j/_sync/work/result.py @@ -19,7 +19,10 @@ from __future__ import annotations import typing as t -from collections import deque +from collections import ( + deque, + namedtuple, +) from warnings import warn @@ -33,6 +36,7 @@ RecordTableRowExporter, ) from ..._meta import experimental +from ..._work import EagerResult from ...exceptions import ( ResultConsumedError, ResultNotSingleError, @@ -581,6 +585,28 @@ def data(self, *keys: _T_ResultKey) -> t.List[t.Any]: """ return [record.data(*keys) for record in self] + def to_eager_result(self) -> EagerResult: + """Convert this result to an :class:`.EagerResult`. + + This method exhausts the result and triggers a :meth:`.consume`. + + :returns: all remaining records in the result stream, the result's + summary, and keys as an :class:`.EagerResult` instance. + + :raises ResultConsumedError: if the transaction from which this result + was obtained has been closed or the Result has been explicitly + consumed. + + .. versionadded:: 5.2 + """ + + self._buffer_all() + return EagerResult( + keys=list(self.keys()), + records=Util.list(self), + summary=self.consume() + ) + def to_df( self, expand: bool = False, @@ -711,9 +737,9 @@ def to_df( df[dt_columns] = df[dt_columns].apply( lambda col: col.map( lambda x: - pd.Timestamp(x.iso_format()) - .replace(tzinfo=getattr(x, "tzinfo", None)) - if x else pd.NaT + pd.Timestamp(x.iso_format()) + .replace(tzinfo=getattr(x, "tzinfo", None)) + if x else pd.NaT ) ) return df diff --git a/neo4j/_work/__init__.py b/neo4j/_work/__init__.py new file mode 100644 index 000000000..15d85d129 --- /dev/null +++ b/neo4j/_work/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://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 +# +# https://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. + + +from ._eager_result import EagerResult + + +__all__ = [ + "EagerResult", +] diff --git a/neo4j/_work/_eager_result.py b/neo4j/_work/_eager_result.py new file mode 100644 index 000000000..2d433cafa --- /dev/null +++ b/neo4j/_work/_eager_result.py @@ -0,0 +1,50 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://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 +# +# https://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. + + +import typing as t + +from .._data import Record +from ..work import ResultSummary + + +class EagerResult(t.NamedTuple): + """In-memory result of a query. + + It's a named tuple with 3 elements: + * records - the list of records returned by the query + (list of :class:`.Record` objects) + * summary - the summary of the query execution + (:class:`.ResultSummary` object) + * keys - the list of keys returned by the query + (see :attr:`AsyncResult.keys` and :attr:`.Result.keys`) + + .. seealso:: + :attr:`.AsyncDriver.execute_query`, :attr:`.Driver.execute_query` + Which by default return an instance of this class. + + :attr:`.AsyncResult.to_eager_result`, :attr:`.Result.to_eager_result` + Which can be used to convert to instance of this class. + + .. versionadded:: 5.2 + """ + #: Alias for field 0 (``eager_result[0]``) + records: t.List[Record] + #: Alias for field 1 (``eager_result[1]``) + summary: ResultSummary + #: Alias for field 2 (``eager_result[2]``) + keys: t.List[str] diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index ac491fb23..20ffcf1dd 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -188,6 +188,35 @@ async def CheckMultiDBSupport(backend, data): }) +async def ExecuteQuery(backend, data): + driver = backend.drivers[data["driverId"]] + cypher, params = fromtestkit.to_cypher_and_params(data) + config = data.get("config", {}) + kwargs = {} + for config_key, kwargs_key in ( + ("database", "database"), + ("routing", "routing"), + ("impersonatedUser", "impersonated_user"), + ): + value = config.get(config_key, None) + if value is not None: + kwargs[kwargs_key] = value + bookmark_manager_id = config.get("bookmarkManagerId") + if bookmark_manager_id is not None: + if bookmark_manager_id == -1: + kwargs["bookmark_manager"] = None + else: + bookmark_manager = backend.bookmark_managers[bookmark_manager_id] + kwargs["bookmark_manager"] = bookmark_manager + + eager_result = await driver.execute_query(cypher, params, **kwargs) + await backend.send_response("EagerResult", { + "keys": eager_result.keys, + "records": list(map(totestkit.record, eager_result.records)), + "summary": totestkit.summary(eager_result.summary), + }) + + def resolution_func(backend, custom_resolver=False, custom_dns_resolver=False): # This solution (putting custom resolution together with DNS resolution # into one function only works because the Python driver calls the custom @@ -586,42 +615,7 @@ async def ResultConsume(backend, data): summary = await result.consume() from neo4j import ResultSummary assert isinstance(summary, ResultSummary) - await backend.send_response("Summary", { - "serverInfo": { - "address": ":".join(map(str, summary.server.address)), - "agent": summary.server.agent, - "protocolVersion": - ".".join(map(str, summary.server.protocol_version)), - }, - "counters": None if not summary.counters else { - "constraintsAdded": summary.counters.constraints_added, - "constraintsRemoved": summary.counters.constraints_removed, - "containsSystemUpdates": summary.counters.contains_system_updates, - "containsUpdates": summary.counters.contains_updates, - "indexesAdded": summary.counters.indexes_added, - "indexesRemoved": summary.counters.indexes_removed, - "labelsAdded": summary.counters.labels_added, - "labelsRemoved": summary.counters.labels_removed, - "nodesCreated": summary.counters.nodes_created, - "nodesDeleted": summary.counters.nodes_deleted, - "propertiesSet": summary.counters.properties_set, - "relationshipsCreated": summary.counters.relationships_created, - "relationshipsDeleted": summary.counters.relationships_deleted, - "systemUpdates": summary.counters.system_updates, - }, - "database": summary.database, - "notifications": summary.notifications, - "plan": summary.plan, - "profile": summary.profile, - "query": { - "text": summary.query, - "parameters": {k: totestkit.field(v) - for k, v in summary.parameters.items()}, - }, - "queryType": summary.query_type, - "resultAvailableAfter": summary.result_available_after, - "resultConsumedAfter": summary.result_consumed_after, - }) + await backend.send_response("Summary", totestkit.summary(summary)) async def ForcedRoutingTableUpdate(backend, data): diff --git a/testkitbackend/_sync/requests.py b/testkitbackend/_sync/requests.py index d226d1525..f6a5986d0 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -188,6 +188,35 @@ def CheckMultiDBSupport(backend, data): }) +def ExecuteQuery(backend, data): + driver = backend.drivers[data["driverId"]] + cypher, params = fromtestkit.to_cypher_and_params(data) + config = data.get("config", {}) + kwargs = {} + for config_key, kwargs_key in ( + ("database", "database"), + ("routing", "routing"), + ("impersonatedUser", "impersonated_user"), + ): + value = config.get(config_key, None) + if value is not None: + kwargs[kwargs_key] = value + bookmark_manager_id = config.get("bookmarkManagerId") + if bookmark_manager_id is not None: + if bookmark_manager_id == -1: + kwargs["bookmark_manager"] = None + else: + bookmark_manager = backend.bookmark_managers[bookmark_manager_id] + kwargs["bookmark_manager"] = bookmark_manager + + eager_result = driver.execute_query(cypher, params, **kwargs) + backend.send_response("EagerResult", { + "keys": eager_result.keys, + "records": list(map(totestkit.record, eager_result.records)), + "summary": totestkit.summary(eager_result.summary), + }) + + def resolution_func(backend, custom_resolver=False, custom_dns_resolver=False): # This solution (putting custom resolution together with DNS resolution # into one function only works because the Python driver calls the custom @@ -586,42 +615,7 @@ def ResultConsume(backend, data): summary = result.consume() from neo4j import ResultSummary assert isinstance(summary, ResultSummary) - backend.send_response("Summary", { - "serverInfo": { - "address": ":".join(map(str, summary.server.address)), - "agent": summary.server.agent, - "protocolVersion": - ".".join(map(str, summary.server.protocol_version)), - }, - "counters": None if not summary.counters else { - "constraintsAdded": summary.counters.constraints_added, - "constraintsRemoved": summary.counters.constraints_removed, - "containsSystemUpdates": summary.counters.contains_system_updates, - "containsUpdates": summary.counters.contains_updates, - "indexesAdded": summary.counters.indexes_added, - "indexesRemoved": summary.counters.indexes_removed, - "labelsAdded": summary.counters.labels_added, - "labelsRemoved": summary.counters.labels_removed, - "nodesCreated": summary.counters.nodes_created, - "nodesDeleted": summary.counters.nodes_deleted, - "propertiesSet": summary.counters.properties_set, - "relationshipsCreated": summary.counters.relationships_created, - "relationshipsDeleted": summary.counters.relationships_deleted, - "systemUpdates": summary.counters.system_updates, - }, - "database": summary.database, - "notifications": summary.notifications, - "plan": summary.plan, - "profile": summary.profile, - "query": { - "text": summary.query, - "parameters": {k: totestkit.field(v) - for k, v in summary.parameters.items()}, - }, - "queryType": summary.query_type, - "resultAvailableAfter": summary.result_available_after, - "resultConsumedAfter": summary.result_consumed_after, - }) + backend.send_response("Summary", totestkit.summary(summary)) def ForcedRoutingTableUpdate(backend, data): diff --git a/testkitbackend/test_config.json b/testkitbackend/test_config.json index 18710693f..d8d8bc3e7 100644 --- a/testkitbackend/test_config.json +++ b/testkitbackend/test_config.json @@ -18,6 +18,7 @@ "features": { "Feature:API:BookmarkManager": true, "Feature:API:ConnectionAcquisitionTimeout": true, + "Feature:API:Driver.ExecuteQuery": true, "Feature:API:Driver:GetServerInfo": true, "Feature:API:Driver.IsEncrypted": true, "Feature:API:Driver.VerifyConnectivity": true, diff --git a/testkitbackend/totestkit.py b/testkitbackend/totestkit.py index 180f5b4e8..fadc8b5fe 100644 --- a/testkitbackend/totestkit.py +++ b/testkitbackend/totestkit.py @@ -18,6 +18,7 @@ import math +import neo4j from neo4j.graph import ( Node, Path, @@ -44,6 +45,45 @@ def record(rec): return {"values": fields} +def summary(summary_: neo4j.ResultSummary) -> dict: + return { + "serverInfo": { + "address": ":".join(map(str, summary_.server.address)), + "agent": summary_.server.agent, + "protocolVersion": + ".".join(map(str, summary_.server.protocol_version)), + }, + "counters": None if not summary_.counters else { + "constraintsAdded": summary_.counters.constraints_added, + "constraintsRemoved": summary_.counters.constraints_removed, + "containsSystemUpdates": summary_.counters.contains_system_updates, + "containsUpdates": summary_.counters.contains_updates, + "indexesAdded": summary_.counters.indexes_added, + "indexesRemoved": summary_.counters.indexes_removed, + "labelsAdded": summary_.counters.labels_added, + "labelsRemoved": summary_.counters.labels_removed, + "nodesCreated": summary_.counters.nodes_created, + "nodesDeleted": summary_.counters.nodes_deleted, + "propertiesSet": summary_.counters.properties_set, + "relationshipsCreated": summary_.counters.relationships_created, + "relationshipsDeleted": summary_.counters.relationships_deleted, + "systemUpdates": summary_.counters.system_updates, + }, + "database": summary_.database, + "notifications": summary_.notifications, + "plan": summary_.plan, + "profile": summary_.profile, + "query": { + "text": summary_.query, + "parameters": {k: field(v) + for k, v in (summary_.parameters or {}).items()}, + }, + "queryType": summary_.query_type, + "resultAvailableAfter": summary_.result_available_after, + "resultConsumedAfter": summary_.result_consumed_after, + } + + def field(v): def to(name, val): return {"name": name, "data": {"value": val}} diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 2a5acf7b9..8f09c54d3 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -22,11 +22,14 @@ import typing as t import pytest +import typing_extensions as te +import neo4j from neo4j import ( AsyncBoltDriver, AsyncGraphDatabase, AsyncNeo4jDriver, + AsyncResult, ExperimentalWarning, TRUST_ALL_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, @@ -34,6 +37,7 @@ TrustCustomCAs, TrustSystemCAs, ) +from neo4j._async.driver import _work from neo4j.api import ( AsyncBookmarkManager, BookmarkManager, @@ -416,3 +420,271 @@ def forget(self, databases: t.Iterable[str]) -> None: _ = driver.session(bookmark_manager=bmm) session_cls_mock.assert_called_once() assert session_cls_mock.call_args[0][1].bookmark_manager is bmm + + +class SomeClass: + pass + + +@mark_async_test +async def test_execute_query_work(mocker): + tx_mock = mocker.AsyncMock(spec=neo4j.AsyncManagedTransaction) + transformer_mock = mocker.AsyncMock() + transformer: t.Callable[[AsyncResult], t.Awaitable[SomeClass]] = \ + transformer_mock + query = "QUERY" + parameters = {"para": "meters", "foo": object} + + res: SomeClass = await _work(tx_mock, query, parameters, transformer) + + tx_mock.run.assert_awaited_once_with(query, parameters) + transformer_mock.assert_awaited_once_with(tx_mock.run.return_value) + assert res is transformer_mock.return_value + + +@pytest.mark.parametrize("query", ("foo", "bar", "RETURN 1 AS n")) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_query( + mocker, query: str, positional: bool +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if positional: + res = await driver.execute_query(query) + else: + res = await driver.execute_query(query=query) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__aenter__.assert_awaited_once() + session_mock.__aexit__.assert_awaited_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_awaited_once_with( + _work, query, mocker.ANY, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize("parameters", ( + ..., None, {}, {"foo": 1}, {"foo": 1, "bar": object()} +)) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_parameters( + mocker, parameters: t.Optional[t.Dict[str, t.Any]], + positional: bool +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if parameters is Ellipsis: + parameters = None + res = await driver.execute_query("") + else: + if positional: + res = await driver.execute_query("", parameters) + else: + res = await driver.execute_query("", parameters=parameters) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__aenter__.assert_awaited_once() + session_mock.__aexit__.assert_awaited_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_awaited_once_with( + _work, mocker.ANY, parameters or {}, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize("parameters", ( + None, {}, {"foo": 1}, {"foo": 1, "bar": object()} +)) +@mark_async_test +async def test_execute_query_keyword_parameters( + mocker, parameters: t.Optional[t.Dict[str, t.Any]], +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if parameters is None: + res = await driver.execute_query("") + else: + res = await driver.execute_query("", **parameters) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__aenter__.assert_awaited_once() + session_mock.__aexit__.assert_awaited_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_awaited_once_with( + _work, mocker.ANY, parameters or {}, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize( + ("routing_mode", "session_executor"), + ( + (None, "execute_write"), + ("r", "execute_read"), + ("w", "execute_write"), + (neo4j.RoutingControl.READERS, "execute_read"), + (neo4j.RoutingControl.WRITERS, "execute_write"), + ) +) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_routing_control( + mocker, session_executor: str, positional: bool, + routing_mode: t.Union[neo4j.RoutingControl, te.Literal["r", "w"], None] +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if routing_mode is None: + res = await driver.execute_query("") + else: + if positional: + res = await driver.execute_query("", None, routing_mode) + else: + res = await driver.execute_query("", routing=routing_mode) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__aenter__.assert_awaited_once() + session_mock.__aexit__.assert_awaited_once() + session_executor_mock = getattr(session_mock, session_executor) + session_executor_mock.assert_awaited_once_with( + _work, mocker.ANY, mocker.ANY, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize("database", ( + ..., None, "foo", "baz", "neo4j", "system" +)) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_database( + mocker, database: t.Optional[str], positional: bool +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if database is Ellipsis: + database = None + await driver.execute_query("") + else: + if positional: + await driver.execute_query("", None, "w", database) + else: + await driver.execute_query("", database=database) + + session_cls_mock.assert_called_once() + session_config = session_cls_mock.call_args.args[1] + assert session_config.database == database + + +@pytest.mark.parametrize("impersonated_user", (..., None, "foo", "baz")) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_impersonated_user( + mocker, impersonated_user: t.Optional[str], positional: bool +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if impersonated_user is Ellipsis: + impersonated_user = None + await driver.execute_query("") + else: + if positional: + await driver.execute_query( + "", None, "w", None, impersonated_user + ) + else: + await driver.execute_query( + "", impersonated_user=impersonated_user + ) + + session_cls_mock.assert_called_once() + session_config = session_cls_mock.call_args.args[1] + assert session_config.impersonated_user == impersonated_user + + +@pytest.mark.parametrize("bookmark_manager", (..., None, object())) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_bookmark_manager( + mocker, positional: bool, + bookmark_manager: t.Union[AsyncBookmarkManager, BookmarkManager, None] +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if bookmark_manager is Ellipsis: + bookmark_manager = driver.query_bookmark_manager + await driver.execute_query("") + else: + if positional: + await driver.execute_query( + "", None, "w", None, None, bookmark_manager + ) + else: + await driver.execute_query( + "", bookmark_manager=bookmark_manager + ) + + session_cls_mock.assert_called_once() + session_config = session_cls_mock.call_args.args[1] + assert session_config.bookmark_manager == bookmark_manager + + +@pytest.mark.parametrize("result_transformer", (..., object())) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_result_transformer( + mocker, positional: bool, + result_transformer: t.Callable[[AsyncResult], t.Awaitable[SomeClass]] +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + res: t.Any + async with driver as driver: + if result_transformer is Ellipsis: + result_transformer = AsyncResult.to_eager_result + res_default: neo4j.EagerResult = await driver.execute_query("") + res = res_default + else: + res_custom: SomeClass + if positional: + res_custom = await driver.execute_query( + "", None, "w", None, None, driver.query_bookmark_manager, + result_transformer + ) + else: + res_custom = await driver.execute_query( + "", result_transformer=result_transformer + ) + res = res_custom + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__aenter__.assert_awaited_once() + session_mock.__aexit__.assert_awaited_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_awaited_once_with( + _work, mocker.ANY, mocker.ANY, result_transformer + ) + assert res is session_executor_mock.return_value diff --git a/tests/unit/async_/work/test_result.py b/tests/unit/async_/work/test_result.py index 4c46e3c12..ca1b11557 100644 --- a/tests/unit/async_/work/test_result.py +++ b/tests/unit/async_/work/test_result.py @@ -14,8 +14,7 @@ # 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. - - +import uuid import warnings from unittest import mock @@ -26,7 +25,7 @@ from neo4j import ( Address, AsyncResult, - ExperimentalWarning, + EagerResult, Record, ResultSummary, ServerInfo, @@ -687,6 +686,49 @@ async def test_result_graph(records): assert rel["since"] == 1999 +@pytest.mark.parametrize("records", ( + Records(["n"], []), + Records(["n"], [[42], [69], [420], [1337]]), + Records(["n1", "r", "n2"], [ + [ + # Node + Structure(b"N", 0, ["Person", "LabelTest1"], {"name": "Alice"}), + # Relationship + Structure(b"R", 0, 0, 1, "KNOWS", {"since": 1999}), + # Node + Structure(b"N", 1, ["Person", "LabelTest2"], {"name": "Bob"}), + ] + ]), + Records(["secret_sauce"], [[object()], [object()]]), +)) +@mark_async_test +async def test_to_eager_result(records): + summary = {"test_to_eager_result": uuid.uuid4()} + connection = AsyncConnectionStub(records=records, summary_meta=summary) + result = AsyncResult(connection, 1, noop, noop) + await result._run("CYPHER", {}, None, None, "r", None) + eager_result = await result.to_eager_result() + + assert isinstance(eager_result, EagerResult) + + assert eager_result.records is eager_result[0] + assert isinstance(eager_result.records, list) + assert all(isinstance(r, Record) for r in eager_result.records) + assert len(eager_result.records) == len(records) + assert all(list(record) == list(raw) + for record, raw in zip(eager_result.records, records)) + + assert eager_result.summary is eager_result[1] + assert isinstance(eager_result.summary, ResultSummary) + assert (eager_result.summary.metadata.get("test_to_eager_result") + == summary["test_to_eager_result"]) + + assert eager_result.keys is eager_result[2] + assert isinstance(eager_result.keys, list) + assert all(isinstance(k, str) for k in eager_result.keys) + assert eager_result.keys == list(records.fields) + + @pytest.mark.parametrize( ("keys", "values", "types", "instances"), ( diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index 34e38d80b..ca8993ef8 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -22,18 +22,22 @@ import typing as t import pytest +import typing_extensions as te +import neo4j from neo4j import ( BoltDriver, ExperimentalWarning, GraphDatabase, Neo4jDriver, + Result, TRUST_ALL_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, TrustAll, TrustCustomCAs, TrustSystemCAs, ) +from neo4j._sync.driver import _work from neo4j.api import ( BookmarkManager, READ_ACCESS, @@ -415,3 +419,271 @@ def forget(self, databases: t.Iterable[str]) -> None: _ = driver.session(bookmark_manager=bmm) session_cls_mock.assert_called_once() assert session_cls_mock.call_args[0][1].bookmark_manager is bmm + + +class SomeClass: + pass + + +@mark_sync_test +def test_execute_query_work(mocker): + tx_mock = mocker.Mock(spec=neo4j.ManagedTransaction) + transformer_mock = mocker.Mock() + transformer: t.Callable[[Result], t.Union[SomeClass]] = \ + transformer_mock + query = "QUERY" + parameters = {"para": "meters", "foo": object} + + res: SomeClass = _work(tx_mock, query, parameters, transformer) + + tx_mock.run.assert_called_once_with(query, parameters) + transformer_mock.assert_called_once_with(tx_mock.run.return_value) + assert res is transformer_mock.return_value + + +@pytest.mark.parametrize("query", ("foo", "bar", "RETURN 1 AS n")) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_query( + mocker, query: str, positional: bool +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if positional: + res = driver.execute_query(query) + else: + res = driver.execute_query(query=query) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__enter__.assert_called_once() + session_mock.__exit__.assert_called_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_called_once_with( + _work, query, mocker.ANY, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize("parameters", ( + ..., None, {}, {"foo": 1}, {"foo": 1, "bar": object()} +)) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_parameters( + mocker, parameters: t.Optional[t.Dict[str, t.Any]], + positional: bool +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if parameters is Ellipsis: + parameters = None + res = driver.execute_query("") + else: + if positional: + res = driver.execute_query("", parameters) + else: + res = driver.execute_query("", parameters=parameters) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__enter__.assert_called_once() + session_mock.__exit__.assert_called_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_called_once_with( + _work, mocker.ANY, parameters or {}, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize("parameters", ( + None, {}, {"foo": 1}, {"foo": 1, "bar": object()} +)) +@mark_sync_test +def test_execute_query_keyword_parameters( + mocker, parameters: t.Optional[t.Dict[str, t.Any]], +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if parameters is None: + res = driver.execute_query("") + else: + res = driver.execute_query("", **parameters) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__enter__.assert_called_once() + session_mock.__exit__.assert_called_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_called_once_with( + _work, mocker.ANY, parameters or {}, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize( + ("routing_mode", "session_executor"), + ( + (None, "execute_write"), + ("r", "execute_read"), + ("w", "execute_write"), + (neo4j.RoutingControl.READERS, "execute_read"), + (neo4j.RoutingControl.WRITERS, "execute_write"), + ) +) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_routing_control( + mocker, session_executor: str, positional: bool, + routing_mode: t.Union[neo4j.RoutingControl, te.Literal["r", "w"], None] +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if routing_mode is None: + res = driver.execute_query("") + else: + if positional: + res = driver.execute_query("", None, routing_mode) + else: + res = driver.execute_query("", routing=routing_mode) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__enter__.assert_called_once() + session_mock.__exit__.assert_called_once() + session_executor_mock = getattr(session_mock, session_executor) + session_executor_mock.assert_called_once_with( + _work, mocker.ANY, mocker.ANY, mocker.ANY + ) + assert res is session_executor_mock.return_value + + +@pytest.mark.parametrize("database", ( + ..., None, "foo", "baz", "neo4j", "system" +)) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_database( + mocker, database: t.Optional[str], positional: bool +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if database is Ellipsis: + database = None + driver.execute_query("") + else: + if positional: + driver.execute_query("", None, "w", database) + else: + driver.execute_query("", database=database) + + session_cls_mock.assert_called_once() + session_config = session_cls_mock.call_args.args[1] + assert session_config.database == database + + +@pytest.mark.parametrize("impersonated_user", (..., None, "foo", "baz")) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_impersonated_user( + mocker, impersonated_user: t.Optional[str], positional: bool +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if impersonated_user is Ellipsis: + impersonated_user = None + driver.execute_query("") + else: + if positional: + driver.execute_query( + "", None, "w", None, impersonated_user + ) + else: + driver.execute_query( + "", impersonated_user=impersonated_user + ) + + session_cls_mock.assert_called_once() + session_config = session_cls_mock.call_args.args[1] + assert session_config.impersonated_user == impersonated_user + + +@pytest.mark.parametrize("bookmark_manager", (..., None, object())) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_bookmark_manager( + mocker, positional: bool, + bookmark_manager: t.Union[BookmarkManager, BookmarkManager, None] +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if bookmark_manager is Ellipsis: + bookmark_manager = driver.query_bookmark_manager + driver.execute_query("") + else: + if positional: + driver.execute_query( + "", None, "w", None, None, bookmark_manager + ) + else: + driver.execute_query( + "", bookmark_manager=bookmark_manager + ) + + session_cls_mock.assert_called_once() + session_config = session_cls_mock.call_args.args[1] + assert session_config.bookmark_manager == bookmark_manager + + +@pytest.mark.parametrize("result_transformer", (..., object())) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_result_transformer( + mocker, positional: bool, + result_transformer: t.Callable[[Result], t.Union[SomeClass]] +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + res: t.Any + with driver as driver: + if result_transformer is Ellipsis: + result_transformer = Result.to_eager_result + res_default: neo4j.EagerResult = driver.execute_query("") + res = res_default + else: + res_custom: SomeClass + if positional: + res_custom = driver.execute_query( + "", None, "w", None, None, driver.query_bookmark_manager, + result_transformer + ) + else: + res_custom = driver.execute_query( + "", result_transformer=result_transformer + ) + res = res_custom + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__enter__.assert_called_once() + session_mock.__exit__.assert_called_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_called_once_with( + _work, mocker.ANY, mocker.ANY, result_transformer + ) + assert res is session_executor_mock.return_value diff --git a/tests/unit/sync/work/test_result.py b/tests/unit/sync/work/test_result.py index cc65a17c2..21a5f06ea 100644 --- a/tests/unit/sync/work/test_result.py +++ b/tests/unit/sync/work/test_result.py @@ -14,8 +14,7 @@ # 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. - - +import uuid import warnings from unittest import mock @@ -25,7 +24,7 @@ from neo4j import ( Address, - ExperimentalWarning, + EagerResult, Record, Result, ResultSummary, @@ -687,6 +686,49 @@ def test_result_graph(records): assert rel["since"] == 1999 +@pytest.mark.parametrize("records", ( + Records(["n"], []), + Records(["n"], [[42], [69], [420], [1337]]), + Records(["n1", "r", "n2"], [ + [ + # Node + Structure(b"N", 0, ["Person", "LabelTest1"], {"name": "Alice"}), + # Relationship + Structure(b"R", 0, 0, 1, "KNOWS", {"since": 1999}), + # Node + Structure(b"N", 1, ["Person", "LabelTest2"], {"name": "Bob"}), + ] + ]), + Records(["secret_sauce"], [[object()], [object()]]), +)) +@mark_sync_test +def test_to_eager_result(records): + summary = {"test_to_eager_result": uuid.uuid4()} + connection = ConnectionStub(records=records, summary_meta=summary) + result = Result(connection, 1, noop, noop) + result._run("CYPHER", {}, None, None, "r", None) + eager_result = result.to_eager_result() + + assert isinstance(eager_result, EagerResult) + + assert eager_result.records is eager_result[0] + assert isinstance(eager_result.records, list) + assert all(isinstance(r, Record) for r in eager_result.records) + assert len(eager_result.records) == len(records) + assert all(list(record) == list(raw) + for record, raw in zip(eager_result.records, records)) + + assert eager_result.summary is eager_result[1] + assert isinstance(eager_result.summary, ResultSummary) + assert (eager_result.summary.metadata.get("test_to_eager_result") + == summary["test_to_eager_result"]) + + assert eager_result.keys is eager_result[2] + assert isinstance(eager_result.keys, list) + assert all(isinstance(k, str) for k in eager_result.keys) + assert eager_result.keys == list(records.fields) + + @pytest.mark.parametrize( ("keys", "values", "types", "instances"), ( From 4125a1c4e86d65c82a00c1c13908cc4ee8ea2ae9 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 19 Oct 2022 11:33:56 +0200 Subject: [PATCH 02/15] TestKit backend can handle omitted query params --- testkitbackend/fromtestkit.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/testkitbackend/fromtestkit.py b/testkitbackend/fromtestkit.py index a320ea780..fcfb97b19 100644 --- a/testkitbackend/fromtestkit.py +++ b/testkitbackend/fromtestkit.py @@ -34,14 +34,11 @@ def to_cypher_and_params(data): - from .backend import Request - params = data["params"] - # Optional - if params is None: - return data["cypher"], None - # Transform the params to Python native - params_dict = {p: to_param(params[p]) for p in params} - return data["cypher"], params_dict + params = data.get("params") + if params is not None: + # Transform the params to Python native + params = {p: to_param(params[p]) for p in params} + return data["cypher"], params def to_tx_kwargs(data): From ef90e4a98c9700d8d0e2f76a48267310c17c302e Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 19 Oct 2022 13:44:21 +0200 Subject: [PATCH 03/15] Add tests for query parameter precedence --- tests/unit/async_/test_driver.py | 56 ++++++++++++++++++++++++++++++++ tests/unit/sync/test_driver.py | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 8f09c54d3..c5f77e6a0 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -528,6 +528,62 @@ async def test_execute_query_keyword_parameters( assert res is session_executor_mock.return_value +@pytest.mark.parametrize( + ("params", "kw_params", "expected_params"), + ( + ({"x": 1}, {}, {"x": 1}), + ({}, {"x": 1}, {"x": 1}), + (None, {"x": 1}, {"x": 1}), + ({"x": 1}, {"y": 2}, {"x": 1, "y": 2}), + ({"x": 1}, {"x": 2}, {"x": 2}), + ({"x": 1}, {"x": 2}, {"x": 2}), + ({"x": 1, "y": 3}, {"x": 2}, {"x": 2, "y": 3}), + ({"x": 1}, {"x": 2, "y": 3}, {"x": 2, "y": 3}), + # potentially internally used keyword arguments + ({}, {"timeout": 2}, {"timeout": 2}), + ({"timeout": 2}, {}, {"timeout": 2}), + ({}, {"imp_user": "hans"}, {"imp_user": "hans"}), + ({"imp_user": "hans"}, {}, {"imp_user": "hans"}), + ({}, {"db": "neo4j"}, {"db": "neo4j"}), + ({"db": "neo4j"}, {}, {"db": "neo4j"}), + # already taken keyword arguments + ({}, {"database": "neo4j"}, {}), + ({"database": "neo4j"}, {}, {"database": "neo4j"}), + ) +) +@pytest.mark.parametrize("positional", (True, False)) +@mark_async_test +async def test_execute_query_parameter_precedence( + params: t.Optional[t.Dict[str, t.Any]], + kw_params: t.Dict[str, t.Any], + expected_params: t.Dict[str, t.Any], + positional: bool, + mocker +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", + autospec=True) + async with driver as driver: + if params is None: + res = await driver.execute_query("", **kw_params) + else: + if positional: + res = await driver.execute_query("", params, **kw_params) + else: + res = await driver.execute_query("", parameters=params, + **kw_params) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__aenter__.assert_awaited_once() + session_mock.__aexit__.assert_awaited_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_awaited_once_with( + _work, mocker.ANY, expected_params, mocker.ANY + ) + assert res is session_executor_mock.return_value + + @pytest.mark.parametrize( ("routing_mode", "session_executor"), ( diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index ca8993ef8..073d661ea 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -527,6 +527,62 @@ def test_execute_query_keyword_parameters( assert res is session_executor_mock.return_value +@pytest.mark.parametrize( + ("params", "kw_params", "expected_params"), + ( + ({"x": 1}, {}, {"x": 1}), + ({}, {"x": 1}, {"x": 1}), + (None, {"x": 1}, {"x": 1}), + ({"x": 1}, {"y": 2}, {"x": 1, "y": 2}), + ({"x": 1}, {"x": 2}, {"x": 2}), + ({"x": 1}, {"x": 2}, {"x": 2}), + ({"x": 1, "y": 3}, {"x": 2}, {"x": 2, "y": 3}), + ({"x": 1}, {"x": 2, "y": 3}, {"x": 2, "y": 3}), + # potentially internally used keyword arguments + ({}, {"timeout": 2}, {"timeout": 2}), + ({"timeout": 2}, {}, {"timeout": 2}), + ({}, {"imp_user": "hans"}, {"imp_user": "hans"}), + ({"imp_user": "hans"}, {}, {"imp_user": "hans"}), + ({}, {"db": "neo4j"}, {"db": "neo4j"}), + ({"db": "neo4j"}, {}, {"db": "neo4j"}), + # already taken keyword arguments + ({}, {"database": "neo4j"}, {}), + ({"database": "neo4j"}, {}, {"database": "neo4j"}), + ) +) +@pytest.mark.parametrize("positional", (True, False)) +@mark_sync_test +def test_execute_query_parameter_precedence( + params: t.Optional[t.Dict[str, t.Any]], + kw_params: t.Dict[str, t.Any], + expected_params: t.Dict[str, t.Any], + positional: bool, + mocker +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + session_cls_mock = mocker.patch("neo4j._sync.driver.Session", + autospec=True) + with driver as driver: + if params is None: + res = driver.execute_query("", **kw_params) + else: + if positional: + res = driver.execute_query("", params, **kw_params) + else: + res = driver.execute_query("", parameters=params, + **kw_params) + + session_cls_mock.assert_called_once() + session_mock = session_cls_mock.return_value + session_mock.__enter__.assert_called_once() + session_mock.__exit__.assert_called_once() + session_executor_mock = session_mock.execute_write + session_executor_mock.assert_called_once_with( + _work, mocker.ANY, expected_params, mocker.ANY + ) + assert res is session_executor_mock.return_value + + @pytest.mark.parametrize( ("routing_mode", "session_executor"), ( From cf786898a3432d43c8cccf3de6c68e30f5d86d12 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 21 Oct 2022 12:56:57 +0200 Subject: [PATCH 04/15] Docs: be more precise about retires --- docs/source/api.rst | 5 ++++- docs/source/async_api.rst | 5 ++++- neo4j/_async/driver.py | 5 ++++- neo4j/_sync/driver.py | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index a24923975..68b9d5027 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -156,13 +156,16 @@ Closing a driver will immediately shut down all connections in the pool. .. method:: execute_query(query, parameters=None,routing=neo4j.RoutingControl.WRITERS, database=None, impersonated_user=None, bookmark_manager=self.query_bookmark_manager, result_transformer=Result.to_eager_result, **kwargs) - Execute a query inside a retired transaction and return all results. + Execute a query in a transaction function and return all results. This method is a handy wrapper for lower-level driver APIs like sessions, transactions, and transaction functions. It is intended for simple use cases where there is no need for managing all possible options. + The internal usage of transaction functions provides a retry-mechanism + for appropriate errors. + The method is roughly equivalent to:: def execute_query( diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 4f76373d8..57dbfc629 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -140,13 +140,16 @@ Closing a driver will immediately shut down all connections in the pool. .. method:: execute_query(query, parameters=None,routing=neo4j.RoutingControl.WRITERS, database=None, impersonated_user=None, bookmark_manager=self.query_bookmark_manager, result_transformer=AsyncResult.to_eager_result, **kwargs) :async: - Execute a query inside a retired transaction and return all results. + Execute a query in a transaction function and return all results. This method is a handy wrapper for lower-level driver APIs like sessions, transactions, and transaction functions. It is intended for simple use cases where there is no need for managing all possible options. + The internal usage of transaction functions provides a retry-mechanism + for appropriate errors. + The method is roughly equivalent to:: async def execute_query( diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index 47fab9f21..d158d82e6 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -540,13 +540,16 @@ async def execute_query( ), **kwargs: t.Any ) -> _T: - """Execute a query inside a retired transaction and return all results. + """Execute a query in a transaction function and return all results. This method is a handy wrapper for lower-level driver APIs like sessions, transactions, and transaction functions. It is intended for simple use cases where there is no need for managing all possible options. + The internal usage of transaction functions provides a retry-mechanism + for appropriate errors. + The method is roughly equivalent to:: async def execute_query( diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index 5fd47d0f7..d6a5c4b7c 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -539,13 +539,16 @@ def execute_query( ), **kwargs: t.Any ) -> _T: - """Execute a query inside a retired transaction and return all results. + """Execute a query in a transaction function and return all results. This method is a handy wrapper for lower-level driver APIs like sessions, transactions, and transaction functions. It is intended for simple use cases where there is no need for managing all possible options. + The internal usage of transaction functions provides a retry-mechanism + for appropriate errors. + The method is roughly equivalent to:: def execute_query( From cdd90103f24be18336e32346413f9310d5938ea2 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 21 Oct 2022 13:19:16 +0200 Subject: [PATCH 05/15] Tested and improved examples in docstrings --- docs/source/api.rst | 15 ++++++++++----- docs/source/async_api.rst | 15 ++++++++++----- neo4j/_async/driver.py | 13 ++++++++----- neo4j/_sync/driver.py | 13 ++++++++----- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 68b9d5027..86729b475 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -192,8 +192,9 @@ Closing a driver will immediately shut down all connections in the pool. import neo4j + def example(driver: neo4j.Driver) -> List[str]: - """Get the name of all 42 year-olds.""" + \"""Get the name of all 42 year-olds.\""" records, summary, keys = driver.execute_query( "MATCH (p:Person {age: $age}) RETURN p.name", {"age": 42}, @@ -201,7 +202,7 @@ Closing a driver will immediately shut down all connections in the pool. database="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration - log.debug("some meta data: %s", summary) + # log_summary(summary) # log some metadata return [str(record["p.name"]) for record in records] # or: return [str(record[0]) for record in records] # or even: return list(map(lambda r: str(r[0]), records)) @@ -210,16 +211,19 @@ Closing a driver will immediately shut down all connections in the pool. import neo4j + def example(driver: neo4j.Driver) -> int: - """Call all young people "My dear" and get their count.""" + \"""Call all young people "My dear" and get their count.\""" record = driver.execute_query( - "MATCH (p:Person) WHERE n.age <= 15 " + "MATCH (p:Person) WHERE p.age <= 15 " "SET p.nickname = 'My dear' " "RETURN count(*)", - routing=neo4j.RoutingControl.WRITERS, # or just "w" + # optional routing parameter, as write is default + # routing=neo4j.RoutingControl.WRITERS, # or just "w", database="neo4j", result_transformer=neo4j.Result.single, ) + assert record is not None # for typechecking and illustration count = record[0] assert isinstance(count, int) return count @@ -271,6 +275,7 @@ Closing a driver will immediately shut down all connections in the pool. import neo4j + def transformer( result: neo4j.Result ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 57dbfc629..36c1e8328 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -176,8 +176,9 @@ Closing a driver will immediately shut down all connections in the pool. import neo4j + async def example(driver: neo4j.AsyncDriver) -> List[str]: - """Get the name of all 42 year-olds.""" + \"""Get the name of all 42 year-olds.\""" records, summary, keys = await driver.execute_query( "MATCH (p:Person {age: $age}) RETURN p.name", {"age": 42}, @@ -185,7 +186,7 @@ Closing a driver will immediately shut down all connections in the pool. database="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration - log.debug("some meta data: %s", summary) + # log_summary(summary) # log some metadata return [str(record["p.name"]) for record in records] # or: return [str(record[0]) for record in records] # or even: return list(map(lambda r: str(r[0]), records)) @@ -194,16 +195,19 @@ Closing a driver will immediately shut down all connections in the pool. import neo4j + async def example(driver: neo4j.AsyncDriver) -> int: - """Call all young people "My dear" and get their count.""" + \"""Call all young people "My dear" and get their count.\""" record = await driver.execute_query( - "MATCH (p:Person) WHERE n.age <= 15 " + "MATCH (p:Person) WHERE p.age <= 15 " "SET p.nickname = 'My dear' " "RETURN count(*)", - routing=neo4j.RoutingControl.WRITERS, # or just "w" + # optional routing parameter, as write is default + # routing=neo4j.RoutingControl.WRITERS, # or just "w", database="neo4j", result_transformer=neo4j.AsyncResult.single, ) + assert record is not None # for typechecking and illustration count = record[0] assert isinstance(count, int) return count @@ -255,6 +259,7 @@ Closing a driver will immediately shut down all connections in the pool. import neo4j + async def transformer( result: neo4j.AsyncResult ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index d158d82e6..14f865ed5 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -576,6 +576,7 @@ async def work(tx): import neo4j + async def example(driver: neo4j.AsyncDriver) -> List[str]: \"""Get the name of all 42 year-olds.\""" records, summary, keys = await driver.execute_query( @@ -585,7 +586,7 @@ async def example(driver: neo4j.AsyncDriver) -> List[str]: database="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration - log.debug("some meta data: %s", summary) + # log_summary(summary) # log some metadata return [str(record["p.name"]) for record in records] # or: return [str(record[0]) for record in records] # or even: return list(map(lambda r: str(r[0]), records)) @@ -594,16 +595,19 @@ async def example(driver: neo4j.AsyncDriver) -> List[str]: import neo4j + async def example(driver: neo4j.AsyncDriver) -> int: \"""Call all young people "My dear" and get their count.\""" record = await driver.execute_query( - "MATCH (p:Person) WHERE n.age <= 15 " + "MATCH (p:Person) WHERE p.age <= 15 " "SET p.nickname = 'My dear' " "RETURN count(*)", - routing=neo4j.RoutingControl.WRITERS, # or just "w" + # optional routing parameter, as write is default + # routing=neo4j.RoutingControl.WRITERS, # or just "w", database="neo4j", result_transformer=neo4j.AsyncResult.single, ) + assert record is not None # for typechecking and illustration count = record[0] assert isinstance(count, int) return count @@ -655,6 +659,7 @@ async def example(driver: neo4j.AsyncDriver) -> int: import neo4j + async def transformer( result: neo4j.AsyncResult ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: @@ -699,8 +704,6 @@ async def transformer( impersonated_user=impersonated_user, bookmark_manager=bookmark_manager) async with session: - # TODO: test all examples in the docstring - if routing == RoutingControl.WRITERS: executor = session.execute_write elif routing == RoutingControl.READERS: diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index d6a5c4b7c..c86f9c6eb 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -575,6 +575,7 @@ def work(tx): import neo4j + def example(driver: neo4j.Driver) -> List[str]: \"""Get the name of all 42 year-olds.\""" records, summary, keys = driver.execute_query( @@ -584,7 +585,7 @@ def example(driver: neo4j.Driver) -> List[str]: database="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration - log.debug("some meta data: %s", summary) + # log_summary(summary) # log some metadata return [str(record["p.name"]) for record in records] # or: return [str(record[0]) for record in records] # or even: return list(map(lambda r: str(r[0]), records)) @@ -593,16 +594,19 @@ def example(driver: neo4j.Driver) -> List[str]: import neo4j + def example(driver: neo4j.Driver) -> int: \"""Call all young people "My dear" and get their count.\""" record = driver.execute_query( - "MATCH (p:Person) WHERE n.age <= 15 " + "MATCH (p:Person) WHERE p.age <= 15 " "SET p.nickname = 'My dear' " "RETURN count(*)", - routing=neo4j.RoutingControl.WRITERS, # or just "w" + # optional routing parameter, as write is default + # routing=neo4j.RoutingControl.WRITERS, # or just "w", database="neo4j", result_transformer=neo4j.Result.single, ) + assert record is not None # for typechecking and illustration count = record[0] assert isinstance(count, int) return count @@ -654,6 +658,7 @@ def example(driver: neo4j.Driver) -> int: import neo4j + def transformer( result: neo4j.Result ) -> Tuple[neo4j.Record, neo4j.ResultSummary]: @@ -698,8 +703,6 @@ def transformer( impersonated_user=impersonated_user, bookmark_manager=bookmark_manager) with session: - # TODO: test all examples in the docstring - if routing == RoutingControl.WRITERS: executor = session.execute_write elif routing == RoutingControl.READERS: From 610261c66e59a71e1c2067a99d4d37f3c2c1fd40 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 24 Oct 2022 08:40:23 +0200 Subject: [PATCH 06/15] API docs: explain auto-commit limitations --- docs/source/api.rst | 4 +++- docs/source/async_api.rst | 4 +++- neo4j/_async/driver.py | 4 +++- neo4j/_sync/driver.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 86729b475..43d421e93 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -164,7 +164,9 @@ Closing a driver will immediately shut down all connections in the pool. options. The internal usage of transaction functions provides a retry-mechanism - for appropriate errors. + for appropriate errors. Furthermore, this means that queries using + ``CALL {} IN TRANSACTIONS`` or the older ``USING PERIODIC COMMIT`` + will not work (use :meth:`Session.run` for these). The method is roughly equivalent to:: diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 36c1e8328..0541a4524 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -148,7 +148,9 @@ Closing a driver will immediately shut down all connections in the pool. options. The internal usage of transaction functions provides a retry-mechanism - for appropriate errors. + for appropriate errors. Furthermore, this means that queries using + ``CALL {} IN TRANSACTIONS`` or the older ``USING PERIODIC COMMIT`` + will not work (use :meth:`AsyncSession.run` for these). The method is roughly equivalent to:: diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index 14f865ed5..87ef89eb2 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -548,7 +548,9 @@ async def execute_query( options. The internal usage of transaction functions provides a retry-mechanism - for appropriate errors. + for appropriate errors. Furthermore, this means that queries using + ``CALL {} IN TRANSACTIONS`` or the older ``USING PERIODIC COMMIT`` + will not work (use :meth:`AsyncSession.run` for these). The method is roughly equivalent to:: diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index c86f9c6eb..febd8be2a 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -547,7 +547,9 @@ def execute_query( options. The internal usage of transaction functions provides a retry-mechanism - for appropriate errors. + for appropriate errors. Furthermore, this means that queries using + ``CALL {} IN TRANSACTIONS`` or the older ``USING PERIODIC COMMIT`` + will not work (use :meth:`Session.run` for these). The method is roughly equivalent to:: From d3a4aa57ae0ffc71eea96c832d96b220ed662a2e Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 24 Oct 2022 11:52:19 +0200 Subject: [PATCH 07/15] API docs: fix missing ref --- docs/source/api.rst | 2 +- docs/source/async_api.rst | 2 +- neo4j/_async/driver.py | 2 +- neo4j/_sync/driver.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 43d421e93..9d2562226 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -258,7 +258,7 @@ Closing a driver will immediately shut down all connections in the pool. :class:`Driver` has been created needs to have the appropriate permissions. - See also the Session config + See also the Session config :ref:`impersonated-user-ref`. :type impersonated_user: typing.Optional[str] :param result_transformer: A function that gets passed the :class:`neo4j.Result` object diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 0541a4524..05fb75975 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -242,7 +242,7 @@ Closing a driver will immediately shut down all connections in the pool. :class:`Driver` has been created needs to have the appropriate permissions. - See also the Session config + See also the Session config :ref:`impersonated-user-ref`. :type impersonated_user: typing.Optional[str] :param result_transformer: A function that gets passed the :class:`neo4j.AsyncResult` object diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index 87ef89eb2..eeac9a500 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -642,7 +642,7 @@ async def example(driver: neo4j.AsyncDriver) -> int: :class:`Driver` has been created needs to have the appropriate permissions. - See also the Session config + See also the Session config :ref:`impersonated-user-ref`. :type impersonated_user: typing.Optional[str] :param result_transformer: A function that gets passed the :class:`neo4j.AsyncResult` object diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index febd8be2a..2021739a0 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -641,7 +641,7 @@ def example(driver: neo4j.Driver) -> int: :class:`Driver` has been created needs to have the appropriate permissions. - See also the Session config + See also the Session config :ref:`impersonated-user-ref`. :type impersonated_user: typing.Optional[str] :param result_transformer: A function that gets passed the :class:`neo4j.Result` object From bd8fcabd82da7df03b5b74c3bc9b2d8893511e80 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 24 Oct 2022 16:08:46 +0200 Subject: [PATCH 08/15] Leave extension points in execute_query API --- docs/source/api.rst | 35 +++++---- docs/source/async_api.rst | 35 +++++---- neo4j/_async/driver.py | 122 +++++++++++++++++-------------- neo4j/_sync/driver.py | 122 +++++++++++++++++-------------- tests/unit/async_/test_driver.py | 43 ++++++++--- tests/unit/sync/test_driver.py | 43 ++++++++--- 6 files changed, 238 insertions(+), 162 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 9d2562226..2ca85269c 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -230,15 +230,15 @@ 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.Optional[str] - :param parameters: parameters to use in the query - :type parameters: typing.Optional[typing.Dict[str, typing.Any]] - :param routing: + :param query_: cypher query to execute + :type query_: typing.Optional[str] + :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 a writer (leader) in the cluster. Default is to route to a writer. - :type routing: neo4j.RoutingControl - :param database: + :type routing_: neo4j.RoutingControl + :param database_: database to execute the query against. None (default) uses the database configured on the server side. @@ -249,8 +249,8 @@ Closing a driver will immediately shut down all connections in the pool. as it will not have to resolve the default database first. See also the Session config :ref:`database-ref`. - :type database: typing.Optional[str] - :param impersonated_user: + :type database_: typing.Optional[str] + :param impersonated_user_: Name of the user to impersonate. This means that all query will be executed in the security context @@ -259,8 +259,8 @@ Closing a driver will immediately shut down all connections in the pool. permissions. See also the Session config :ref:`impersonated-user-ref`. - :type impersonated_user: typing.Optional[str] - :param result_transformer: + :type impersonated_user_: typing.Optional[str] + :param result_transformer_: A function that gets passed the :class:`neo4j.Result` object resulting from the query and converts it to a different type. The result of the transformer function is returned by this method. @@ -285,9 +285,9 @@ Closing a driver will immediately shut down all connections in the pool. summary = result.consume() return record, summary - :type result_transformer: + :type result_transformer_: typing.Callable[[neo4j.Result], typing.Union[T]] - :param bookmark_manager: + :param bookmark_manager_: Specify a bookmark manager to use. If present, the bookmark manager is used to keep the query causally @@ -296,11 +296,14 @@ Closing a driver will immediately shut down all connections in the pool. Defaults to the driver's :attr:`.query_bookmark_manager`. Pass :const:`None` to disable causal consistency. - :type bookmark_manager: + :type bookmark_manager_: typing.Union[neo4j.BookmarkManager, neo4j.BookmarkManager, None] - :param kwargs: additional keyword parameters. - These take precedence over parameters passed as ``parameters``. + :param kwargs: additional keyword parameters. None of these can end + with a single underscore. This is to avoid collisions with the + keyword configuration parameters of this method. If you need to + pass such a parameter, use the ``parameters_`` parameter instead. + These take precedence over parameters passed as ``parameters_``. :type kwargs: typing.Any :returns: the result of the ``result_transformer`` diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 05fb75975..1e987213c 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -214,15 +214,15 @@ 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.Optional[str] - :param parameters: parameters to use in the query - :type parameters: typing.Optional[typing.Dict[str, typing.Any]] - :param routing: + :param query_: cypher query to execute + :type query_: typing.Optional[str] + :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 a writer (leader) in the cluster. Default is to route to a writer. - :type routing: neo4j.RoutingControl - :param database: + :type routing_: neo4j.RoutingControl + :param database_: database to execute the query against. None (default) uses the database configured on the server side. @@ -233,8 +233,8 @@ Closing a driver will immediately shut down all connections in the pool. as it will not have to resolve the default database first. See also the Session config :ref:`database-ref`. - :type database: typing.Optional[str] - :param impersonated_user: + :type database_: typing.Optional[str] + :param impersonated_user_: Name of the user to impersonate. This means that all query will be executed in the security context @@ -243,8 +243,8 @@ Closing a driver will immediately shut down all connections in the pool. permissions. See also the Session config :ref:`impersonated-user-ref`. - :type impersonated_user: typing.Optional[str] - :param result_transformer: + :type impersonated_user_: typing.Optional[str] + :param result_transformer_: A function that gets passed the :class:`neo4j.AsyncResult` object resulting from the query and converts it to a different type. The result of the transformer function is returned by this method. @@ -269,9 +269,9 @@ Closing a driver will immediately shut down all connections in the pool. summary = await result.consume() return record, summary - :type result_transformer: + :type result_transformer_: typing.Callable[[neo4j.AsyncResult], typing.Awaitable[T]] - :param bookmark_manager: + :param bookmark_manager_: Specify a bookmark manager to use. If present, the bookmark manager is used to keep the query causally @@ -280,11 +280,14 @@ Closing a driver will immediately shut down all connections in the pool. Defaults to the driver's :attr:`.query_bookmark_manager`. Pass :const:`None` to disable causal consistency. - :type bookmark_manager: + :type bookmark_manager_: typing.Union[neo4j.AsyncBookmarkManager, neo4j.BookmarkManager, None] - :param kwargs: additional keyword parameters. - These take precedence over parameters passed as ``parameters``. + :param kwargs: additional keyword parameters. None of these can end + with a single underscore. This is to avoid collisions with the + keyword configuration parameters of this method. If you need to + pass such a parameter, use the ``parameters_`` parameter instead. + These take precedence over parameters passed as ``parameters_``. :type kwargs: typing.Any :returns: the result of the ``result_transformer`` diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index eeac9a500..e45a1b73a 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -489,15 +489,15 @@ async def close(self) -> None: @t.overload async def execute_query( self, - query: str, - parameters: t.Dict[str, t.Any] = None, - routing: T_RoutingControl = RoutingControl.WRITERS, - database: str = None, - impersonated_user: str = None, - bookmark_manager: t.Union[ + query_: str, + parameters_: t.Dict[str, t.Any] = None, + routing_: T_RoutingControl = RoutingControl.WRITERS, + database_: str = None, + impersonated_user_: str = None, + bookmark_manager_: t.Union[ AsyncBookmarkManager, BookmarkManager, None ] = ..., - result_transformer: t.Callable[ + result_transformer_: t.Callable[ [AsyncResult], t.Awaitable[EagerResult] ] = ..., **kwargs: t.Any @@ -507,15 +507,15 @@ async def execute_query( @t.overload async def execute_query( self, - query: str, - parameters: t.Dict[str, t.Any] = None, - routing: T_RoutingControl = RoutingControl.WRITERS, - database: str = None, - impersonated_user: str = None, - bookmark_manager: t.Union[ + query_: str, + parameters_: t.Dict[str, t.Any] = None, + routing_: T_RoutingControl = RoutingControl.WRITERS, + database_: str = None, + impersonated_user_: str = None, + bookmark_manager_: t.Union[ AsyncBookmarkManager, BookmarkManager, None ] = ..., - result_transformer: t.Callable[ + result_transformer_: t.Callable[ [AsyncResult], t.Awaitable[_T] ] = ..., **kwargs: t.Any @@ -524,22 +524,20 @@ async def execute_query( async def execute_query( self, - query: str, - parameters: t.Dict[str, t.Any] = None, - routing: T_RoutingControl = RoutingControl.WRITERS, - database: str = None, - impersonated_user: str = None, - bookmark_manager: t.Union[ + query_: str, + parameters_: t.Dict[str, t.Any] = None, + routing_: T_RoutingControl = RoutingControl.WRITERS, + database_: str = None, + impersonated_user_: str = None, + bookmark_manager_: t.Union[ AsyncBookmarkManager, BookmarkManager, None, te.Literal[_DefaultEnum.default] ] = _default, - result_transformer: t.Callable[[AsyncResult], t.Awaitable[_T]] = ( - # cast to work around https://github.com/python/mypy/issues/3737 - t.cast(t.Callable[[AsyncResult], t.Awaitable[_T]], - AsyncResult.to_eager_result) - ), + result_transformer_: t.Callable[ + [AsyncResult], t.Awaitable[t.Any] + ] = AsyncResult.to_eager_result, **kwargs: t.Any - ) -> _T: + ) -> t.Any: """Execute a query in a transaction function and return all results. This method is a handy wrapper for lower-level driver APIs like @@ -614,15 +612,15 @@ async def example(driver: neo4j.AsyncDriver) -> int: assert isinstance(count, int) return count - :param query: cypher query to execute - :type query: typing.Optional[str] - :param parameters: parameters to use in the query - :type parameters: typing.Optional[typing.Dict[str, typing.Any]] - :param routing: + :param query_: cypher query to execute + :type query_: typing.Optional[str] + :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 a writer (leader) in the cluster. Default is to route to a writer. - :type routing: neo4j.RoutingControl - :param database: + :type routing_: neo4j.RoutingControl + :param database_: database to execute the query against. None (default) uses the database configured on the server side. @@ -633,8 +631,8 @@ async def example(driver: neo4j.AsyncDriver) -> int: as it will not have to resolve the default database first. See also the Session config :ref:`database-ref`. - :type database: typing.Optional[str] - :param impersonated_user: + :type database_: typing.Optional[str] + :param impersonated_user_: Name of the user to impersonate. This means that all query will be executed in the security context @@ -643,8 +641,8 @@ async def example(driver: neo4j.AsyncDriver) -> int: permissions. See also the Session config :ref:`impersonated-user-ref`. - :type impersonated_user: typing.Optional[str] - :param result_transformer: + :type impersonated_user_: typing.Optional[str] + :param result_transformer_: A function that gets passed the :class:`neo4j.AsyncResult` object resulting from the query and converts it to a different type. The result of the transformer function is returned by this method. @@ -669,9 +667,9 @@ async def transformer( summary = await result.consume() return record, summary - :type result_transformer: + :type result_transformer_: typing.Callable[[neo4j.AsyncResult], typing.Awaitable[T]] - :param bookmark_manager: + :param bookmark_manager_: Specify a bookmark manager to use. If present, the bookmark manager is used to keep the query causally @@ -680,11 +678,14 @@ async def transformer( Defaults to the driver's :attr:`.query_bookmark_manager`. Pass :const:`None` to disable causal consistency. - :type bookmark_manager: + :type bookmark_manager_: typing.Union[neo4j.AsyncBookmarkManager, neo4j.BookmarkManager, None] - :param kwargs: additional keyword parameters. - These take precedence over parameters passed as ``parameters``. + :param kwargs: additional keyword parameters. None of these can end + with a single underscore. This is to avoid collisions with the + keyword configuration parameters of this method. If you need to + pass such a parameter, use the ``parameters_`` parameter instead. + These take precedence over parameters passed as ``parameters_``. :type kwargs: typing.Any :returns: the result of the ``result_transformer`` @@ -692,27 +693,40 @@ async def transformer( .. versionadded:: 5.2 """ - parameters = dict(parameters or {}, **kwargs) - - if bookmark_manager is _default: - bookmark_manager = self._query_bookmark_manager - assert bookmark_manager is not _default + invalid_kwargs = [k for k in kwargs if + k[-2:-1] != "_" and k[-1:] == "_"] + if invalid_kwargs: + raise ValueError( + "keyword parameters must not end with a single '_'. Found: %r" + "\nYou either misspelled an existing configuration parameter " + "or tried to send a query parameter that is reserved. In the " + "latter case, use the `parameters_` dictionary instead." + % invalid_kwargs + ) + parameters = dict(parameters_ or {}, **kwargs) + + if bookmark_manager_ is _default: + bookmark_manager_ = self._query_bookmark_manager + assert bookmark_manager_ is not _default with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=r".*\bbookmark_manager\b.*", category=ExperimentalWarning) - session = self.session(database=database, - impersonated_user=impersonated_user, - bookmark_manager=bookmark_manager) + session = self.session(database=database_, + impersonated_user=impersonated_user_, + bookmark_manager=bookmark_manager_) async with session: - if routing == RoutingControl.WRITERS: + if routing_ == RoutingControl.WRITERS: executor = session.execute_write - elif routing == RoutingControl.READERS: + elif routing_ == RoutingControl.READERS: executor = session.execute_read else: - raise ValueError("Invalid routing control value: %r" % routing) - return await executor(_work, query, parameters, result_transformer) + raise ValueError("Invalid routing control value: %r" + % routing_) + return await executor( + _work, query_, parameters, result_transformer_ + ) @property def query_bookmark_manager(self) -> AsyncBookmarkManager: diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index 2021739a0..4756c43f1 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -488,15 +488,15 @@ def close(self) -> None: @t.overload def execute_query( self, - query: str, - parameters: t.Dict[str, t.Any] = None, - routing: T_RoutingControl = RoutingControl.WRITERS, - database: str = None, - impersonated_user: str = None, - bookmark_manager: t.Union[ + query_: str, + parameters_: t.Dict[str, t.Any] = None, + routing_: T_RoutingControl = RoutingControl.WRITERS, + database_: str = None, + impersonated_user_: str = None, + bookmark_manager_: t.Union[ BookmarkManager, BookmarkManager, None ] = ..., - result_transformer: t.Callable[ + result_transformer_: t.Callable[ [Result], t.Union[EagerResult] ] = ..., **kwargs: t.Any @@ -506,15 +506,15 @@ def execute_query( @t.overload def execute_query( self, - query: str, - parameters: t.Dict[str, t.Any] = None, - routing: T_RoutingControl = RoutingControl.WRITERS, - database: str = None, - impersonated_user: str = None, - bookmark_manager: t.Union[ + query_: str, + parameters_: t.Dict[str, t.Any] = None, + routing_: T_RoutingControl = RoutingControl.WRITERS, + database_: str = None, + impersonated_user_: str = None, + bookmark_manager_: t.Union[ BookmarkManager, BookmarkManager, None ] = ..., - result_transformer: t.Callable[ + result_transformer_: t.Callable[ [Result], t.Union[_T] ] = ..., **kwargs: t.Any @@ -523,22 +523,20 @@ def execute_query( def execute_query( self, - query: str, - parameters: t.Dict[str, t.Any] = None, - routing: T_RoutingControl = RoutingControl.WRITERS, - database: str = None, - impersonated_user: str = None, - bookmark_manager: t.Union[ + query_: str, + parameters_: t.Dict[str, t.Any] = None, + routing_: T_RoutingControl = RoutingControl.WRITERS, + database_: str = None, + impersonated_user_: str = None, + bookmark_manager_: t.Union[ BookmarkManager, BookmarkManager, None, te.Literal[_DefaultEnum.default] ] = _default, - result_transformer: t.Callable[[Result], t.Union[_T]] = ( - # cast to work around https://github.com/python/mypy/issues/3737 - t.cast(t.Callable[[Result], t.Union[_T]], - Result.to_eager_result) - ), + result_transformer_: t.Callable[ + [Result], t.Union[t.Any] + ] = Result.to_eager_result, **kwargs: t.Any - ) -> _T: + ) -> t.Any: """Execute a query in a transaction function and return all results. This method is a handy wrapper for lower-level driver APIs like @@ -613,15 +611,15 @@ def example(driver: neo4j.Driver) -> int: assert isinstance(count, int) return count - :param query: cypher query to execute - :type query: typing.Optional[str] - :param parameters: parameters to use in the query - :type parameters: typing.Optional[typing.Dict[str, typing.Any]] - :param routing: + :param query_: cypher query to execute + :type query_: typing.Optional[str] + :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 a writer (leader) in the cluster. Default is to route to a writer. - :type routing: neo4j.RoutingControl - :param database: + :type routing_: neo4j.RoutingControl + :param database_: database to execute the query against. None (default) uses the database configured on the server side. @@ -632,8 +630,8 @@ def example(driver: neo4j.Driver) -> int: as it will not have to resolve the default database first. See also the Session config :ref:`database-ref`. - :type database: typing.Optional[str] - :param impersonated_user: + :type database_: typing.Optional[str] + :param impersonated_user_: Name of the user to impersonate. This means that all query will be executed in the security context @@ -642,8 +640,8 @@ def example(driver: neo4j.Driver) -> int: permissions. See also the Session config :ref:`impersonated-user-ref`. - :type impersonated_user: typing.Optional[str] - :param result_transformer: + :type impersonated_user_: typing.Optional[str] + :param result_transformer_: A function that gets passed the :class:`neo4j.Result` object resulting from the query and converts it to a different type. The result of the transformer function is returned by this method. @@ -668,9 +666,9 @@ def transformer( summary = result.consume() return record, summary - :type result_transformer: + :type result_transformer_: typing.Callable[[neo4j.Result], typing.Union[T]] - :param bookmark_manager: + :param bookmark_manager_: Specify a bookmark manager to use. If present, the bookmark manager is used to keep the query causally @@ -679,11 +677,14 @@ def transformer( Defaults to the driver's :attr:`.query_bookmark_manager`. Pass :const:`None` to disable causal consistency. - :type bookmark_manager: + :type bookmark_manager_: typing.Union[neo4j.BookmarkManager, neo4j.BookmarkManager, None] - :param kwargs: additional keyword parameters. - These take precedence over parameters passed as ``parameters``. + :param kwargs: additional keyword parameters. None of these can end + with a single underscore. This is to avoid collisions with the + keyword configuration parameters of this method. If you need to + pass such a parameter, use the ``parameters_`` parameter instead. + These take precedence over parameters passed as ``parameters_``. :type kwargs: typing.Any :returns: the result of the ``result_transformer`` @@ -691,27 +692,40 @@ def transformer( .. versionadded:: 5.2 """ - parameters = dict(parameters or {}, **kwargs) - - if bookmark_manager is _default: - bookmark_manager = self._query_bookmark_manager - assert bookmark_manager is not _default + invalid_kwargs = [k for k in kwargs if + k[-2:-1] != "_" and k[-1:] == "_"] + if invalid_kwargs: + raise ValueError( + "keyword parameters must not end with a single '_'. Found: %r" + "\nYou either misspelled an existing configuration parameter " + "or tried to send a query parameter that is reserved. In the " + "latter case, use the `parameters_` dictionary instead." + % invalid_kwargs + ) + parameters = dict(parameters_ or {}, **kwargs) + + if bookmark_manager_ is _default: + bookmark_manager_ = self._query_bookmark_manager + assert bookmark_manager_ is not _default with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=r".*\bbookmark_manager\b.*", category=ExperimentalWarning) - session = self.session(database=database, - impersonated_user=impersonated_user, - bookmark_manager=bookmark_manager) + session = self.session(database=database_, + impersonated_user=impersonated_user_, + bookmark_manager=bookmark_manager_) with session: - if routing == RoutingControl.WRITERS: + if routing_ == RoutingControl.WRITERS: executor = session.execute_write - elif routing == RoutingControl.READERS: + elif routing_ == RoutingControl.READERS: executor = session.execute_read else: - raise ValueError("Invalid routing control value: %r" % routing) - return executor(_work, query, parameters, result_transformer) + raise ValueError("Invalid routing control value: %r" + % routing_) + return executor( + _work, query_, parameters, result_transformer_ + ) @property def query_bookmark_manager(self) -> BookmarkManager: diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index c5f77e6a0..a78504222 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -455,7 +455,7 @@ async def test_execute_query_query( if positional: res = await driver.execute_query(query) else: - res = await driver.execute_query(query=query) + res = await driver.execute_query(query_=query) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -488,7 +488,7 @@ async def test_execute_query_parameters( if positional: res = await driver.execute_query("", parameters) else: - res = await driver.execute_query("", parameters=parameters) + res = await driver.execute_query("", parameters_=parameters) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -502,7 +502,7 @@ async def test_execute_query_parameters( @pytest.mark.parametrize("parameters", ( - None, {}, {"foo": 1}, {"foo": 1, "bar": object()} + None, {}, {"foo": 1}, {"foo": 1, "_bar": object()}, {"__": 1}, {"baz__": 2} )) @mark_async_test async def test_execute_query_keyword_parameters( @@ -528,6 +528,21 @@ async def test_execute_query_keyword_parameters( assert res is session_executor_mock.return_value +@pytest.mark.parametrize("parameters", ( + {"_": "a"}, {"foo_": None}, {"foo_": 1, "bar_": 2} +)) +async def test_reserved_query_keyword_parameters( + mocker, parameters: t.Dict[str, t.Any], +) -> None: + driver = AsyncGraphDatabase.driver("bolt://localhost") + mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) + async with driver as driver: + with pytest.raises(ValueError) as exc: + await driver.execute_query("", **parameters) + exc.match("reserved") + exc.match(", ".join(f"'{k}'" for k in parameters)) + + @pytest.mark.parametrize( ("params", "kw_params", "expected_params"), ( @@ -546,9 +561,15 @@ async def test_execute_query_keyword_parameters( ({"imp_user": "hans"}, {}, {"imp_user": "hans"}), ({}, {"db": "neo4j"}, {"db": "neo4j"}), ({"db": "neo4j"}, {}, {"db": "neo4j"}), - # already taken keyword arguments - ({}, {"database": "neo4j"}, {}), + ({"_": "foobar"}, {}, {"_": "foobar"}), + ({"__": "foobar"}, {}, {"__": "foobar"}), + ({"x_": "foobar"}, {}, {"x_": "foobar"}), + ({"x__": "foobar"}, {}, {"x__": "foobar"}), + ({}, {"database": "neo4j"}, {"database": "neo4j"}), ({"database": "neo4j"}, {}, {"database": "neo4j"}), + # already taken keyword arguments + ({}, {"database_": "neo4j"}, {}), + ({"database_": "neo4j"}, {}, {"database_": "neo4j"}), ) ) @pytest.mark.parametrize("positional", (True, False)) @@ -570,7 +591,7 @@ async def test_execute_query_parameter_precedence( if positional: res = await driver.execute_query("", params, **kw_params) else: - res = await driver.execute_query("", parameters=params, + res = await driver.execute_query("", parameters_=params, **kw_params) session_cls_mock.assert_called_once() @@ -610,7 +631,7 @@ async def test_execute_query_routing_control( if positional: res = await driver.execute_query("", None, routing_mode) else: - res = await driver.execute_query("", routing=routing_mode) + res = await driver.execute_query("", routing_=routing_mode) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -642,7 +663,7 @@ async def test_execute_query_database( if positional: await driver.execute_query("", None, "w", database) else: - await driver.execute_query("", database=database) + await driver.execute_query("", database_=database) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -669,7 +690,7 @@ async def test_execute_query_impersonated_user( ) else: await driver.execute_query( - "", impersonated_user=impersonated_user + "", impersonated_user_=impersonated_user ) session_cls_mock.assert_called_once() @@ -698,7 +719,7 @@ async def test_execute_query_bookmark_manager( ) else: await driver.execute_query( - "", bookmark_manager=bookmark_manager + "", bookmark_manager_=bookmark_manager ) session_cls_mock.assert_called_once() @@ -731,7 +752,7 @@ async def test_execute_query_result_transformer( ) else: res_custom = await driver.execute_query( - "", result_transformer=result_transformer + "", result_transformer_=result_transformer ) res = res_custom diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index 073d661ea..ee92f9311 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -454,7 +454,7 @@ def test_execute_query_query( if positional: res = driver.execute_query(query) else: - res = driver.execute_query(query=query) + res = driver.execute_query(query_=query) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -487,7 +487,7 @@ def test_execute_query_parameters( if positional: res = driver.execute_query("", parameters) else: - res = driver.execute_query("", parameters=parameters) + res = driver.execute_query("", parameters_=parameters) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -501,7 +501,7 @@ def test_execute_query_parameters( @pytest.mark.parametrize("parameters", ( - None, {}, {"foo": 1}, {"foo": 1, "bar": object()} + None, {}, {"foo": 1}, {"foo": 1, "_bar": object()}, {"__": 1}, {"baz__": 2} )) @mark_sync_test def test_execute_query_keyword_parameters( @@ -527,6 +527,21 @@ def test_execute_query_keyword_parameters( assert res is session_executor_mock.return_value +@pytest.mark.parametrize("parameters", ( + {"_": "a"}, {"foo_": None}, {"foo_": 1, "bar_": 2} +)) +def test_reserved_query_keyword_parameters( + mocker, parameters: t.Dict[str, t.Any], +) -> None: + driver = GraphDatabase.driver("bolt://localhost") + mocker.patch("neo4j._sync.driver.Session", autospec=True) + with driver as driver: + with pytest.raises(ValueError) as exc: + driver.execute_query("", **parameters) + exc.match("reserved") + exc.match(", ".join(f"'{k}'" for k in parameters)) + + @pytest.mark.parametrize( ("params", "kw_params", "expected_params"), ( @@ -545,9 +560,15 @@ def test_execute_query_keyword_parameters( ({"imp_user": "hans"}, {}, {"imp_user": "hans"}), ({}, {"db": "neo4j"}, {"db": "neo4j"}), ({"db": "neo4j"}, {}, {"db": "neo4j"}), - # already taken keyword arguments - ({}, {"database": "neo4j"}, {}), + ({"_": "foobar"}, {}, {"_": "foobar"}), + ({"__": "foobar"}, {}, {"__": "foobar"}), + ({"x_": "foobar"}, {}, {"x_": "foobar"}), + ({"x__": "foobar"}, {}, {"x__": "foobar"}), + ({}, {"database": "neo4j"}, {"database": "neo4j"}), ({"database": "neo4j"}, {}, {"database": "neo4j"}), + # already taken keyword arguments + ({}, {"database_": "neo4j"}, {}), + ({"database_": "neo4j"}, {}, {"database_": "neo4j"}), ) ) @pytest.mark.parametrize("positional", (True, False)) @@ -569,7 +590,7 @@ def test_execute_query_parameter_precedence( if positional: res = driver.execute_query("", params, **kw_params) else: - res = driver.execute_query("", parameters=params, + res = driver.execute_query("", parameters_=params, **kw_params) session_cls_mock.assert_called_once() @@ -609,7 +630,7 @@ def test_execute_query_routing_control( if positional: res = driver.execute_query("", None, routing_mode) else: - res = driver.execute_query("", routing=routing_mode) + res = driver.execute_query("", routing_=routing_mode) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -641,7 +662,7 @@ def test_execute_query_database( if positional: driver.execute_query("", None, "w", database) else: - driver.execute_query("", database=database) + driver.execute_query("", database_=database) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -668,7 +689,7 @@ def test_execute_query_impersonated_user( ) else: driver.execute_query( - "", impersonated_user=impersonated_user + "", impersonated_user_=impersonated_user ) session_cls_mock.assert_called_once() @@ -697,7 +718,7 @@ def test_execute_query_bookmark_manager( ) else: driver.execute_query( - "", bookmark_manager=bookmark_manager + "", bookmark_manager_=bookmark_manager ) session_cls_mock.assert_called_once() @@ -730,7 +751,7 @@ def test_execute_query_result_transformer( ) else: res_custom = driver.execute_query( - "", result_transformer=result_transformer + "", result_transformer_=result_transformer ) res = res_custom From 7ff6803df86c8699052333c7cf386ff17ebc226b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 28 Oct 2022 14:57:29 +0200 Subject: [PATCH 09/15] Fix API docs: params have trailing underscore now --- docs/source/api.rst | 33 +++++++++++++++++---------------- docs/source/async_api.rst | 30 +++++++++++++++--------------- neo4j/_async/driver.py | 28 ++++++++++++++-------------- neo4j/_sync/driver.py | 28 ++++++++++++++-------------- 4 files changed, 60 insertions(+), 59 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 2ca85269c..803d81fd2 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -154,7 +154,7 @@ Closing a driver will immediately shut down all connections in the pool. :members: session, query_bookmark_manager, encrypted, close, verify_connectivity, get_server_info - .. method:: execute_query(query, parameters=None,routing=neo4j.RoutingControl.WRITERS, database=None, impersonated_user=None, bookmark_manager=self.query_bookmark_manager, result_transformer=Result.to_eager_result, **kwargs) + .. method:: execute_query(query, parameters_=None,routing_=neo4j.RoutingControl.WRITERS, database_=None, impersonated_user_=None, bookmark_manager_=self.query_bookmark_manager, result_transformer_=Result.to_eager_result, **kwargs) Execute a query in a transaction function and return all results. @@ -171,21 +171,21 @@ Closing a driver will immediately shut down all connections in the pool. The method is roughly equivalent to:: def execute_query( - query, parameters, routing, database, impersonated_user, - bookmark_manager, result_transformer, + query_, parameters_, routing_, database_, impersonated_user_, + bookmark_manager_, result_transformer_, **kwargs ): def work(tx): - result = tx.run(query, parameters) - return some_transformer(result) + result = tx.run(query_, parameters_, **kwargs) + return result_transformer_(result) with driver.session( - database=database, - impersonated_user=impersonated_user, - bookmark_manager=bookmark_manager, + database=database_, + impersonated_user=impersonated_user_, + bookmark_manager=bookmark_manager_, ) as session: - if routing == RoutingControl.WRITERS: + if routing_ == RoutingControl.WRITERS: return session.execute_write(work) - elif routing == RoutingControl.READERS: + elif routing_ == RoutingControl.READERS: return session.execute_read(work) Usage example:: @@ -200,8 +200,8 @@ Closing a driver will immediately shut down all connections in the pool. records, summary, keys = driver.execute_query( "MATCH (p:Person {age: $age}) RETURN p.name", {"age": 42}, - routing=neo4j.RoutingControl.READERS, # or just "r" - database="neo4j", + routing_=neo4j.RoutingControl.READERS, # or just "r" + database_="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration # log_summary(summary) # log some metadata @@ -217,13 +217,14 @@ Closing a driver will immediately shut down all connections in the pool. def example(driver: neo4j.Driver) -> int: \"""Call all young people "My dear" and get their count.\""" record = driver.execute_query( - "MATCH (p:Person) WHERE p.age <= 15 " + "MATCH (p:Person) WHERE p.age <= $age " "SET p.nickname = 'My dear' " "RETURN count(*)", # optional routing parameter, as write is default - # routing=neo4j.RoutingControl.WRITERS, # or just "w", - database="neo4j", - result_transformer=neo4j.Result.single, + # routing_=neo4j.RoutingControl.WRITERS, # or just "w", + database_="neo4j", + result_transformer_=neo4j.Result.single, + age=15, ) assert record is not None # for typechecking and illustration count = record[0] diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 1e987213c..22e64f89e 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -137,7 +137,7 @@ Closing a driver will immediately shut down all connections in the pool. :members: session, query_bookmark_manager, encrypted, close, verify_connectivity, get_server_info - .. method:: execute_query(query, parameters=None,routing=neo4j.RoutingControl.WRITERS, database=None, impersonated_user=None, bookmark_manager=self.query_bookmark_manager, result_transformer=AsyncResult.to_eager_result, **kwargs) + .. method:: execute_query(query, parameters_=None, routing_=neo4j.RoutingControl.WRITERS, database_=None, impersonated_user_=None, bookmark_manager_=self.query_bookmark_manager, result_transformer_=AsyncResult.to_eager_result, **kwargs) :async: Execute a query in a transaction function and return all results. @@ -155,21 +155,21 @@ Closing a driver will immediately shut down all connections in the pool. The method is roughly equivalent to:: async def execute_query( - query, parameters, routing, database, impersonated_user, - bookmark_manager, result_transformer, + query_, parameters_, routing_, database_, impersonated_user_, + bookmark_manager_, result_transformer_, **kwargs ): async def work(tx): - result = await tx.run(query, parameters) - return await some_transformer(result) + result = await tx.run(query_, parameters_, **kwargs) + return await result_transformer_(result) async with driver.session( - database=database, - impersonated_user=impersonated_user, - bookmark_manager=bookmark_manager, + database=database_, + impersonated_user=impersonated_user_, + bookmark_manager=bookmark_manager_, ) as session: - if routing == RoutingControl.WRITERS: + if routing_ == RoutingControl.WRITERS: return await session.execute_write(work) - elif routing == RoutingControl.READERS: + elif routing_ == RoutingControl.READERS: return await session.execute_read(work) Usage example:: @@ -184,8 +184,8 @@ Closing a driver will immediately shut down all connections in the pool. records, summary, keys = await driver.execute_query( "MATCH (p:Person {age: $age}) RETURN p.name", {"age": 42}, - routing=neo4j.RoutingControl.READERS, # or just "r" - database="neo4j", + routing_=neo4j.RoutingControl.READERS, # or just "r" + database_="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration # log_summary(summary) # log some metadata @@ -205,9 +205,9 @@ Closing a driver will immediately shut down all connections in the pool. "SET p.nickname = 'My dear' " "RETURN count(*)", # optional routing parameter, as write is default - # routing=neo4j.RoutingControl.WRITERS, # or just "w", - database="neo4j", - result_transformer=neo4j.AsyncResult.single, + # routing_=neo4j.RoutingControl.WRITERS, # or just "w", + database_="neo4j", + result_transformer_=neo4j.AsyncResult.single, ) assert record is not None # for typechecking and illustration count = record[0] diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index e45a1b73a..02785a6e5 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -553,21 +553,21 @@ async def execute_query( The method is roughly equivalent to:: async def execute_query( - query, parameters, routing, database, impersonated_user, - bookmark_manager, result_transformer, + query_, parameters_, routing_, database_, impersonated_user_, + bookmark_manager_, result_transformer_, **kwargs ): async def work(tx): - result = await tx.run(query, parameters) - return await some_transformer(result) + result = await tx.run(query_, parameters_, **kwargs) + return await result_transformer_(result) async with driver.session( - database=database, - impersonated_user=impersonated_user, - bookmark_manager=bookmark_manager, + database=database_, + impersonated_user=impersonated_user_, + bookmark_manager=bookmark_manager_, ) as session: - if routing == RoutingControl.WRITERS: + if routing_ == RoutingControl.WRITERS: return await session.execute_write(work) - elif routing == RoutingControl.READERS: + elif routing_ == RoutingControl.READERS: return await session.execute_read(work) Usage example:: @@ -582,8 +582,8 @@ async def example(driver: neo4j.AsyncDriver) -> List[str]: records, summary, keys = await driver.execute_query( "MATCH (p:Person {age: $age}) RETURN p.name", {"age": 42}, - routing=neo4j.RoutingControl.READERS, # or just "r" - database="neo4j", + routing_=neo4j.RoutingControl.READERS, # or just "r" + database_="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration # log_summary(summary) # log some metadata @@ -603,9 +603,9 @@ async def example(driver: neo4j.AsyncDriver) -> int: "SET p.nickname = 'My dear' " "RETURN count(*)", # optional routing parameter, as write is default - # routing=neo4j.RoutingControl.WRITERS, # or just "w", - database="neo4j", - result_transformer=neo4j.AsyncResult.single, + # routing_=neo4j.RoutingControl.WRITERS, # or just "w", + database_="neo4j", + result_transformer_=neo4j.AsyncResult.single, ) assert record is not None # for typechecking and illustration count = record[0] diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index 4756c43f1..e22ccf090 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -552,21 +552,21 @@ def execute_query( The method is roughly equivalent to:: def execute_query( - query, parameters, routing, database, impersonated_user, - bookmark_manager, result_transformer, + query_, parameters_, routing_, database_, impersonated_user_, + bookmark_manager_, result_transformer_, **kwargs ): def work(tx): - result = tx.run(query, parameters) - return some_transformer(result) + result = tx.run(query_, parameters_, **kwargs) + return result_transformer_(result) with driver.session( - database=database, - impersonated_user=impersonated_user, - bookmark_manager=bookmark_manager, + database=database_, + impersonated_user=impersonated_user_, + bookmark_manager=bookmark_manager_, ) as session: - if routing == RoutingControl.WRITERS: + if routing_ == RoutingControl.WRITERS: return session.execute_write(work) - elif routing == RoutingControl.READERS: + elif routing_ == RoutingControl.READERS: return session.execute_read(work) Usage example:: @@ -581,8 +581,8 @@ def example(driver: neo4j.Driver) -> List[str]: records, summary, keys = driver.execute_query( "MATCH (p:Person {age: $age}) RETURN p.name", {"age": 42}, - routing=neo4j.RoutingControl.READERS, # or just "r" - database="neo4j", + routing_=neo4j.RoutingControl.READERS, # or just "r" + database_="neo4j", ) assert keys == ["p.name"] # not needed, just for illustration # log_summary(summary) # log some metadata @@ -602,9 +602,9 @@ def example(driver: neo4j.Driver) -> int: "SET p.nickname = 'My dear' " "RETURN count(*)", # optional routing parameter, as write is default - # routing=neo4j.RoutingControl.WRITERS, # or just "w", - database="neo4j", - result_transformer=neo4j.Result.single, + # routing_=neo4j.RoutingControl.WRITERS, # or just "w", + database_="neo4j", + result_transformer_=neo4j.Result.single, ) assert record is not None # for typechecking and illustration count = record[0] From 3cb8c091785ee9bc444a2ebd3618598897d8ea48 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 23 Jan 2023 11:40:40 +0100 Subject: [PATCH 10/15] Update versionadded, mark experimental, fix TestKit backend --- docs/source/api.rst | 32 +++++++++++++++++++++++++- docs/source/async_api.rst | 33 ++++++++++++++++++++++++++- src/neo4j/_api.py | 5 ++++- src/neo4j/_async/driver.py | 37 ++++++++++++++++++++++++++++++- src/neo4j/_async/work/result.py | 5 ++++- src/neo4j/_sync/driver.py | 37 ++++++++++++++++++++++++++++++- src/neo4j/_sync/work/result.py | 5 ++++- src/neo4j/_work/_eager_result.py | 5 ++++- testkitbackend/_async/requests.py | 14 +++++++----- testkitbackend/_sync/requests.py | 14 +++++++----- 10 files changed, 167 insertions(+), 20 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index cb01286a7..21ae22ee3 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -288,6 +288,33 @@ Closing a driver will immediately shut down all connections in the pool. summary = result.consume() return record, summary + Note that methods of :class:`neo4j.Result` that don't take + mandatory arguments can be used directly as transformer functions. + For example:: + + import neo4j + + + def example(driver: neo4j.Driver) -> neo4j.Record:: + record = driver.execute_query( + "SOME QUERY", + result_transformer_=neo4j.Result.single + ) + + + # is equivalent to: + + + def transformer(result: neo4j.Result) -> neo4j.Record: + return result.single() + + + def example(driver: neo4j.Driver) -> neo4j.Record:: + record = driver.execute_query( + "SOME QUERY", + result_transformer_=transformer + ) + :type result_transformer_: typing.Callable[[neo4j.Result], typing.Union[T]] :param bookmark_manager_: @@ -312,7 +339,10 @@ Closing a driver will immediately shut down all connections in the pool. :returns: the result of the ``result_transformer`` :rtype: T - .. versionadded:: 5.2 + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 .. _driver-configuration-ref: diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index b9b546407..f92965d48 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -270,6 +270,33 @@ Closing a driver will immediately shut down all connections in the pool. summary = await result.consume() return record, summary + Note that methods of :class:`neo4j.AsyncResult` that don't take + mandatory arguments can be used directly as transformer functions. + For example:: + + import neo4j + + + async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: + record = await driver.execute_query( + "SOME QUERY", + result_transformer_=neo4j.AsyncResult.single + ) + + + # is equivalent to: + + + async def transformer(result: neo4j.AsyncResult) -> neo4j.Record: + return await result.single() + + + async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: + record = await driver.execute_query( + "SOME QUERY", + result_transformer_=transformer + ) + :type result_transformer_: typing.Callable[[neo4j.AsyncResult], typing.Awaitable[T]] :param bookmark_manager_: @@ -294,7 +321,11 @@ Closing a driver will immediately shut down all connections in the pool. :returns: the result of the ``result_transformer`` :rtype: T - .. versionadded:: 5.2 + + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 .. _async-driver-configuration-ref: diff --git a/src/neo4j/_api.py b/src/neo4j/_api.py index 8c472c23a..75e4b472a 100644 --- a/src/neo4j/_api.py +++ b/src/neo4j/_api.py @@ -37,10 +37,13 @@ class RoutingControl(str, Enum): >>> RoutingControl.WRITERS == "w" True + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + .. seealso:: :attr:`.AsyncDriver.execute_query`, :attr:`.Driver.execute_query` - .. versionadded:: 5.2 + .. versionadded:: 5.5 """ READERS = "r" WRITERS = "w" diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index c61e7b167..87b186070 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -697,6 +697,33 @@ async def transformer( summary = await result.consume() return record, summary + Note that methods of :class:`neo4j.AsyncResult` that don't take + mandatory arguments can be used directly as transformer functions. + For example:: + + import neo4j + + + async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: + record = await driver.execute_query( + "SOME QUERY", + result_transformer_=neo4j.AsyncResult.single + ) + + + # is equivalent to: + + + async def transformer(result: neo4j.AsyncResult) -> neo4j.Record: + return await result.single() + + + async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: + record = await driver.execute_query( + "SOME QUERY", + result_transformer_=transformer + ) + :type result_transformer_: typing.Callable[[neo4j.AsyncResult], typing.Awaitable[T]] :param bookmark_manager_: @@ -721,7 +748,10 @@ async def transformer( :returns: the result of the ``result_transformer`` :rtype: T - .. versionadded:: 5.2 + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 """ invalid_kwargs = [k for k in kwargs if k[-2:-1] != "_" and k[-1:] == "_"] @@ -777,6 +807,11 @@ async def example(driver: neo4j.AsyncDriver) -> None: # subsequent execute_query calls will be causally chained # (i.e., can read what was written by ) await driver.execute_query("") + + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 """ return self._query_bookmark_manager diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index 2a80b7648..f70c2b2c3 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -616,7 +616,10 @@ async def to_eager_result(self) -> EagerResult: was obtained has been closed or the Result has been explicitly consumed. - .. versionadded:: 5.2 + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 """ await self._buffer_all() diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index b6d9285d5..6b214c3c2 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -695,6 +695,33 @@ def transformer( summary = result.consume() return record, summary + Note that methods of :class:`neo4j.Result` that don't take + mandatory arguments can be used directly as transformer functions. + For example:: + + import neo4j + + + def example(driver: neo4j.Driver) -> neo4j.Record:: + record = driver.execute_query( + "SOME QUERY", + result_transformer_=neo4j.Result.single + ) + + + # is equivalent to: + + + def transformer(result: neo4j.Result) -> neo4j.Record: + return result.single() + + + def example(driver: neo4j.Driver) -> neo4j.Record:: + record = driver.execute_query( + "SOME QUERY", + result_transformer_=transformer + ) + :type result_transformer_: typing.Callable[[neo4j.Result], typing.Union[T]] :param bookmark_manager_: @@ -719,7 +746,10 @@ def transformer( :returns: the result of the ``result_transformer`` :rtype: T - .. versionadded:: 5.2 + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 """ invalid_kwargs = [k for k in kwargs if k[-2:-1] != "_" and k[-1:] == "_"] @@ -775,6 +805,11 @@ def example(driver: neo4j.Driver) -> None: # subsequent execute_query calls will be causally chained # (i.e., can read what was written by ) driver.execute_query("") + + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 """ return self._query_bookmark_manager diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index 365f1a041..624acc458 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -616,7 +616,10 @@ def to_eager_result(self) -> EagerResult: was obtained has been closed or the Result has been explicitly consumed. - .. versionadded:: 5.2 + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + + .. versionadded:: 5.5 """ self._buffer_all() diff --git a/src/neo4j/_work/_eager_result.py b/src/neo4j/_work/_eager_result.py index 2d433cafa..525b064f6 100644 --- a/src/neo4j/_work/_eager_result.py +++ b/src/neo4j/_work/_eager_result.py @@ -33,6 +33,9 @@ class EagerResult(t.NamedTuple): * keys - the list of keys returned by the query (see :attr:`AsyncResult.keys` and :attr:`.Result.keys`) + **This is experimental.** (See :ref:`filter-warnings-ref`) + It might be changed or removed any time even without prior notice. + .. seealso:: :attr:`.AsyncDriver.execute_query`, :attr:`.Driver.execute_query` Which by default return an instance of this class. @@ -40,7 +43,7 @@ class EagerResult(t.NamedTuple): :attr:`.AsyncResult.to_eager_result`, :attr:`.Result.to_eager_result` Which can be used to convert to instance of this class. - .. versionadded:: 5.2 + .. versionadded:: 5.5 """ #: Alias for field 0 (``eager_result[0]``) records: t.List[Record] diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index 0841f716a..20218b58e 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -148,8 +148,10 @@ async def NewDriver(backend, data): kwargs["trusted_certificates"] = neo4j.TrustCustomCAs(*cert_paths) data.mark_item_as_read_if_equals("livenessCheckTimeoutMs", None) + kwargs["max_transaction_retry_time"] = 10 + driver = neo4j.AsyncGraphDatabase.driver( - data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs + data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs, ) key = backend.next_key() backend.drivers[key] = driver @@ -194,9 +196,9 @@ async def ExecuteQuery(backend, data): config = data.get("config", {}) kwargs = {} for config_key, kwargs_key in ( - ("database", "database"), - ("routing", "routing"), - ("impersonatedUser", "impersonated_user"), + ("database", "database_"), + ("routing", "routing_"), + ("impersonatedUser", "impersonated_user_"), ): value = config.get(config_key, None) if value is not None: @@ -204,10 +206,10 @@ async def ExecuteQuery(backend, data): bookmark_manager_id = config.get("bookmarkManagerId") if bookmark_manager_id is not None: if bookmark_manager_id == -1: - kwargs["bookmark_manager"] = None + kwargs["bookmark_manager_"] = None else: bookmark_manager = backend.bookmark_managers[bookmark_manager_id] - kwargs["bookmark_manager"] = bookmark_manager + kwargs["bookmark_manager_"] = bookmark_manager eager_result = await driver.execute_query(cypher, params, **kwargs) await backend.send_response("EagerResult", { diff --git a/testkitbackend/_sync/requests.py b/testkitbackend/_sync/requests.py index 095ba8e1d..d6aa0d756 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -148,8 +148,10 @@ def NewDriver(backend, data): kwargs["trusted_certificates"] = neo4j.TrustCustomCAs(*cert_paths) data.mark_item_as_read_if_equals("livenessCheckTimeoutMs", None) + kwargs["max_transaction_retry_time"] = 10 + driver = neo4j.GraphDatabase.driver( - data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs + data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs, ) key = backend.next_key() backend.drivers[key] = driver @@ -194,9 +196,9 @@ def ExecuteQuery(backend, data): config = data.get("config", {}) kwargs = {} for config_key, kwargs_key in ( - ("database", "database"), - ("routing", "routing"), - ("impersonatedUser", "impersonated_user"), + ("database", "database_"), + ("routing", "routing_"), + ("impersonatedUser", "impersonated_user_"), ): value = config.get(config_key, None) if value is not None: @@ -204,10 +206,10 @@ def ExecuteQuery(backend, data): bookmark_manager_id = config.get("bookmarkManagerId") if bookmark_manager_id is not None: if bookmark_manager_id == -1: - kwargs["bookmark_manager"] = None + kwargs["bookmark_manager_"] = None else: bookmark_manager = backend.bookmark_managers[bookmark_manager_id] - kwargs["bookmark_manager"] = bookmark_manager + kwargs["bookmark_manager_"] = bookmark_manager eager_result = driver.execute_query(cypher, params, **kwargs) backend.send_response("EagerResult", { From 530c0e4d268e762ee749366415e5b27bc5b4150b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 24 Jan 2023 11:41:03 +0100 Subject: [PATCH 11/15] Remove debug code --- testkitbackend/_async/requests.py | 2 -- testkitbackend/_sync/requests.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index 20218b58e..6810a0a81 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -148,8 +148,6 @@ async def NewDriver(backend, data): kwargs["trusted_certificates"] = neo4j.TrustCustomCAs(*cert_paths) data.mark_item_as_read_if_equals("livenessCheckTimeoutMs", None) - kwargs["max_transaction_retry_time"] = 10 - driver = neo4j.AsyncGraphDatabase.driver( data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs, ) diff --git a/testkitbackend/_sync/requests.py b/testkitbackend/_sync/requests.py index d6aa0d756..0e03bcf11 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -148,8 +148,6 @@ def NewDriver(backend, data): kwargs["trusted_certificates"] = neo4j.TrustCustomCAs(*cert_paths) data.mark_item_as_read_if_equals("livenessCheckTimeoutMs", None) - kwargs["max_transaction_retry_time"] = 10 - driver = neo4j.GraphDatabase.driver( data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs, ) From 9619e53d1c3df04302e4a2900b0f33b17ff08664 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 24 Jan 2023 14:43:33 +0100 Subject: [PATCH 12/15] Emit experimental warning for Driver.execute_query --- src/neo4j/_api.py | 2 +- src/neo4j/_async/driver.py | 4 + src/neo4j/_sync/driver.py | 4 + testkitbackend/_async/requests.py | 7 +- testkitbackend/_sync/requests.py | 7 +- tests/unit/async_/test_driver.py | 168 +++++++++++++++++------------- tests/unit/sync/test_driver.py | 168 +++++++++++++++++------------- 7 files changed, 211 insertions(+), 149 deletions(-) diff --git a/src/neo4j/_api.py b/src/neo4j/_api.py index 75e4b472a..f9ca49a63 100644 --- a/src/neo4j/_api.py +++ b/src/neo4j/_api.py @@ -37,7 +37,7 @@ class RoutingControl(str, Enum): >>> RoutingControl.WRITERS == "w" True - **This is experimental.** (See :ref:`filter-warnings-ref`) + **This is experimental.** It might be changed or removed any time even without prior notice. .. seealso:: diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index 87b186070..2fed427ef 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -552,6 +552,10 @@ async def execute_query( ) -> _T: ... + @experimental( + "Driver.execute_query is experimental. " + "It might be changed or removed any time even without prior notice." + ) async def execute_query( self, query_: str, diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index 6b214c3c2..8b87b8bed 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -550,6 +550,10 @@ def execute_query( ) -> _T: ... + @experimental( + "Driver.execute_query is experimental. " + "It might be changed or removed any time even without prior notice." + ) def execute_query( self, query_: str, diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index 6810a0a81..7b0f00dbe 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -209,7 +209,12 @@ 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) + with warning_check( + neo4j.ExperimentalWarning, + "Driver.execute_query is experimental. " + "It might be changed or removed any time even without prior notice." + ): + eager_result = await driver.execute_query(cypher, 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 0e03bcf11..854bd9042 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -209,7 +209,12 @@ 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) + with warning_check( + neo4j.ExperimentalWarning, + "Driver.execute_query is experimental. " + "It might be changed or removed any time even without prior notice." + ): + eager_result = driver.execute_query(cypher, 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 41b6fd3da..1edcf22cf 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -20,6 +20,7 @@ import ssl import typing as t +from contextlib import contextmanager import pytest import typing_extensions as te @@ -52,6 +53,16 @@ ) +@contextmanager +def assert_warns_execute_query_experimental(): + with pytest.warns( + ExperimentalWarning, + match=f"^Driver.execute_query is experimental." + ): + yield + + + @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]")) @@ -440,10 +451,11 @@ async def test_execute_query_query( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if positional: - res = await driver.execute_query(query) - else: - res = await driver.execute_query(query_=query) + with assert_warns_execute_query_experimental(): + if positional: + res = await driver.execute_query(query) + else: + res = await driver.execute_query(query_=query) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -469,14 +481,16 @@ async def test_execute_query_parameters( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if parameters is Ellipsis: - parameters = None - res = await driver.execute_query("") - else: - if positional: - res = await driver.execute_query("", parameters) + with assert_warns_execute_query_experimental(): + if parameters is Ellipsis: + parameters = None + res = await driver.execute_query("") else: - res = await driver.execute_query("", parameters_=parameters) + if positional: + res = await driver.execute_query("", parameters) + else: + res = await driver.execute_query("", + parameters_=parameters) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -500,10 +514,11 @@ async def test_execute_query_keyword_parameters( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if parameters is None: - res = await driver.execute_query("") - else: - res = await driver.execute_query("", **parameters) + with assert_warns_execute_query_experimental(): + if parameters is None: + res = await driver.execute_query("") + else: + res = await driver.execute_query("", **parameters) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -526,7 +541,8 @@ async def test_reserved_query_keyword_parameters( mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: with pytest.raises(ValueError) as exc: - await driver.execute_query("", **parameters) + with assert_warns_execute_query_experimental(): + await driver.execute_query("", **parameters) exc.match("reserved") exc.match(", ".join(f"'{k}'" for k in parameters)) @@ -573,14 +589,15 @@ async def test_execute_query_parameter_precedence( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if params is None: - res = await driver.execute_query("", **kw_params) - else: - if positional: - res = await driver.execute_query("", params, **kw_params) + with assert_warns_execute_query_experimental(): + if params is None: + res = await driver.execute_query("", **kw_params) else: - res = await driver.execute_query("", parameters_=params, - **kw_params) + if positional: + res = await driver.execute_query("", params, **kw_params) + else: + res = await driver.execute_query("", parameters_=params, + **kw_params) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -613,13 +630,14 @@ async def test_execute_query_routing_control( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if routing_mode is None: - res = await driver.execute_query("") - else: - if positional: - res = await driver.execute_query("", None, routing_mode) + with assert_warns_execute_query_experimental(): + if routing_mode is None: + res = await driver.execute_query("") else: - res = await driver.execute_query("", routing_=routing_mode) + if positional: + res = await driver.execute_query("", None, routing_mode) + else: + res = await driver.execute_query("", routing_=routing_mode) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -644,14 +662,15 @@ async def test_execute_query_database( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if database is Ellipsis: - database = None - await driver.execute_query("") - else: - if positional: - await driver.execute_query("", None, "w", database) + with assert_warns_execute_query_experimental(): + if database is Ellipsis: + database = None + await driver.execute_query("") else: - await driver.execute_query("", database_=database) + if positional: + await driver.execute_query("", None, "w", database) + else: + await driver.execute_query("", database_=database) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -668,18 +687,19 @@ async def test_execute_query_impersonated_user( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if impersonated_user is Ellipsis: - impersonated_user = None - await driver.execute_query("") - else: - if positional: - await driver.execute_query( - "", None, "w", None, impersonated_user - ) + with assert_warns_execute_query_experimental(): + if impersonated_user is Ellipsis: + impersonated_user = None + await driver.execute_query("") else: - await driver.execute_query( - "", impersonated_user_=impersonated_user - ) + if positional: + await driver.execute_query( + "", None, "w", None, impersonated_user + ) + else: + await driver.execute_query( + "", impersonated_user_=impersonated_user + ) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -697,18 +717,19 @@ async def test_execute_query_bookmark_manager( session_cls_mock = mocker.patch("neo4j._async.driver.AsyncSession", autospec=True) async with driver as driver: - if bookmark_manager is Ellipsis: - bookmark_manager = driver.query_bookmark_manager - await driver.execute_query("") - else: - if positional: - await driver.execute_query( - "", None, "w", None, None, bookmark_manager - ) + with assert_warns_execute_query_experimental(): + if bookmark_manager is Ellipsis: + bookmark_manager = driver.query_bookmark_manager + await driver.execute_query("") else: - await driver.execute_query( - "", bookmark_manager_=bookmark_manager - ) + if positional: + await driver.execute_query( + "", None, "w", None, None, bookmark_manager + ) + else: + await driver.execute_query( + "", bookmark_manager_=bookmark_manager + ) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -727,22 +748,23 @@ async def test_execute_query_result_transformer( autospec=True) res: t.Any async with driver as driver: - if result_transformer is Ellipsis: - result_transformer = AsyncResult.to_eager_result - res_default: neo4j.EagerResult = await driver.execute_query("") - res = res_default - else: - res_custom: SomeClass - if positional: - res_custom = await driver.execute_query( - "", None, "w", None, None, driver.query_bookmark_manager, - result_transformer - ) + with assert_warns_execute_query_experimental(): + if result_transformer is Ellipsis: + result_transformer = AsyncResult.to_eager_result + res_default: neo4j.EagerResult = await driver.execute_query("") + res = res_default else: - res_custom = await driver.execute_query( - "", result_transformer_=result_transformer - ) - res = res_custom + res_custom: SomeClass + if positional: + res_custom = await driver.execute_query( + "", None, "w", None, None, + driver.query_bookmark_manager, result_transformer + ) + else: + res_custom = await driver.execute_query( + "", result_transformer_=result_transformer + ) + res = res_custom session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index db14fe286..ba3867f30 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -20,6 +20,7 @@ import ssl import typing as t +from contextlib import contextmanager import pytest import typing_extensions as te @@ -51,6 +52,16 @@ ) +@contextmanager +def assert_warns_execute_query_experimental(): + with pytest.warns( + ExperimentalWarning, + match=f"^Driver.execute_query is experimental." + ): + yield + + + @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]")) @@ -439,10 +450,11 @@ def test_execute_query_query( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if positional: - res = driver.execute_query(query) - else: - res = driver.execute_query(query_=query) + with assert_warns_execute_query_experimental(): + if positional: + res = driver.execute_query(query) + else: + res = driver.execute_query(query_=query) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -468,14 +480,16 @@ def test_execute_query_parameters( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if parameters is Ellipsis: - parameters = None - res = driver.execute_query("") - else: - if positional: - res = driver.execute_query("", parameters) + with assert_warns_execute_query_experimental(): + if parameters is Ellipsis: + parameters = None + res = driver.execute_query("") else: - res = driver.execute_query("", parameters_=parameters) + if positional: + res = driver.execute_query("", parameters) + else: + res = driver.execute_query("", + parameters_=parameters) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -499,10 +513,11 @@ def test_execute_query_keyword_parameters( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if parameters is None: - res = driver.execute_query("") - else: - res = driver.execute_query("", **parameters) + with assert_warns_execute_query_experimental(): + if parameters is None: + res = driver.execute_query("") + else: + res = driver.execute_query("", **parameters) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -525,7 +540,8 @@ def test_reserved_query_keyword_parameters( mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: with pytest.raises(ValueError) as exc: - driver.execute_query("", **parameters) + with assert_warns_execute_query_experimental(): + driver.execute_query("", **parameters) exc.match("reserved") exc.match(", ".join(f"'{k}'" for k in parameters)) @@ -572,14 +588,15 @@ def test_execute_query_parameter_precedence( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if params is None: - res = driver.execute_query("", **kw_params) - else: - if positional: - res = driver.execute_query("", params, **kw_params) + with assert_warns_execute_query_experimental(): + if params is None: + res = driver.execute_query("", **kw_params) else: - res = driver.execute_query("", parameters_=params, - **kw_params) + if positional: + res = driver.execute_query("", params, **kw_params) + else: + res = driver.execute_query("", parameters_=params, + **kw_params) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -612,13 +629,14 @@ def test_execute_query_routing_control( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if routing_mode is None: - res = driver.execute_query("") - else: - if positional: - res = driver.execute_query("", None, routing_mode) + with assert_warns_execute_query_experimental(): + if routing_mode is None: + res = driver.execute_query("") else: - res = driver.execute_query("", routing_=routing_mode) + if positional: + res = driver.execute_query("", None, routing_mode) + else: + res = driver.execute_query("", routing_=routing_mode) session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value @@ -643,14 +661,15 @@ def test_execute_query_database( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if database is Ellipsis: - database = None - driver.execute_query("") - else: - if positional: - driver.execute_query("", None, "w", database) + with assert_warns_execute_query_experimental(): + if database is Ellipsis: + database = None + driver.execute_query("") else: - driver.execute_query("", database_=database) + if positional: + driver.execute_query("", None, "w", database) + else: + driver.execute_query("", database_=database) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -667,18 +686,19 @@ def test_execute_query_impersonated_user( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if impersonated_user is Ellipsis: - impersonated_user = None - driver.execute_query("") - else: - if positional: - driver.execute_query( - "", None, "w", None, impersonated_user - ) + with assert_warns_execute_query_experimental(): + if impersonated_user is Ellipsis: + impersonated_user = None + driver.execute_query("") else: - driver.execute_query( - "", impersonated_user_=impersonated_user - ) + if positional: + driver.execute_query( + "", None, "w", None, impersonated_user + ) + else: + driver.execute_query( + "", impersonated_user_=impersonated_user + ) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -696,18 +716,19 @@ def test_execute_query_bookmark_manager( session_cls_mock = mocker.patch("neo4j._sync.driver.Session", autospec=True) with driver as driver: - if bookmark_manager is Ellipsis: - bookmark_manager = driver.query_bookmark_manager - driver.execute_query("") - else: - if positional: - driver.execute_query( - "", None, "w", None, None, bookmark_manager - ) + with assert_warns_execute_query_experimental(): + if bookmark_manager is Ellipsis: + bookmark_manager = driver.query_bookmark_manager + driver.execute_query("") else: - driver.execute_query( - "", bookmark_manager_=bookmark_manager - ) + if positional: + driver.execute_query( + "", None, "w", None, None, bookmark_manager + ) + else: + driver.execute_query( + "", bookmark_manager_=bookmark_manager + ) session_cls_mock.assert_called_once() session_config = session_cls_mock.call_args.args[1] @@ -726,22 +747,23 @@ def test_execute_query_result_transformer( autospec=True) res: t.Any with driver as driver: - if result_transformer is Ellipsis: - result_transformer = Result.to_eager_result - res_default: neo4j.EagerResult = driver.execute_query("") - res = res_default - else: - res_custom: SomeClass - if positional: - res_custom = driver.execute_query( - "", None, "w", None, None, driver.query_bookmark_manager, - result_transformer - ) + with assert_warns_execute_query_experimental(): + if result_transformer is Ellipsis: + result_transformer = Result.to_eager_result + res_default: neo4j.EagerResult = driver.execute_query("") + res = res_default else: - res_custom = driver.execute_query( - "", result_transformer_=result_transformer - ) - res = res_custom + res_custom: SomeClass + if positional: + res_custom = driver.execute_query( + "", None, "w", None, None, + driver.query_bookmark_manager, result_transformer + ) + else: + res_custom = driver.execute_query( + "", result_transformer_=result_transformer + ) + res = res_custom session_cls_mock.assert_called_once() session_mock = session_cls_mock.return_value From 44dac2aece038c82f4447feada7a76776c90d562 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 25 Jan 2023 10:17:35 +0100 Subject: [PATCH 13/15] Emit experimental warning for Driver.query_bookmark_manager --- src/neo4j/_async/driver.py | 4 ++++ src/neo4j/_sync/driver.py | 4 ++++ tests/unit/async_/test_driver.py | 17 ++++++++++++++--- tests/unit/sync/test_driver.py | 17 ++++++++++++++--- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index 2fed427ef..4dfca6844 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -793,6 +793,10 @@ async def example(driver: neo4j.AsyncDriver) -> neo4j.Record:: ) @property + @experimental( + "Driver.query_bookmark_manager is experimental. " + "It might be changed or removed any time even without prior notice." + ) def query_bookmark_manager(self) -> AsyncBookmarkManager: """The driver's default query bookmark manager. diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index 8b87b8bed..06ffd957a 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -791,6 +791,10 @@ def example(driver: neo4j.Driver) -> neo4j.Record:: ) @property + @experimental( + "Driver.query_bookmark_manager is experimental. " + "It might be changed or removed any time even without prior notice." + ) def query_bookmark_manager(self) -> BookmarkManager: """The driver's default query bookmark manager. diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 1edcf22cf..10ae46018 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -62,6 +62,15 @@ def assert_warns_execute_query_experimental(): yield +@contextmanager +def assert_warns_execute_query_bmm_experimental(): + with pytest.warns( + ExperimentalWarning, + match=f"^Driver.query_bookmark_manager is experimental." + ): + yield + + @pytest.mark.parametrize("protocol", ("bolt://", "bolt+s://", "bolt+ssc://")) @pytest.mark.parametrize("host", ("localhost", "127.0.0.1", @@ -719,7 +728,8 @@ async def test_execute_query_bookmark_manager( async with driver as driver: with assert_warns_execute_query_experimental(): if bookmark_manager is Ellipsis: - bookmark_manager = driver.query_bookmark_manager + with assert_warns_execute_query_bmm_experimental(): + bookmark_manager = driver.query_bookmark_manager await driver.execute_query("") else: if positional: @@ -756,9 +766,10 @@ async def test_execute_query_result_transformer( else: res_custom: SomeClass if positional: + with assert_warns_execute_query_bmm_experimental(): + bmm = driver.query_bookmark_manager res_custom = await driver.execute_query( - "", None, "w", None, None, - driver.query_bookmark_manager, result_transformer + "", None, "w", None, None, bmm, result_transformer ) else: res_custom = await driver.execute_query( diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index ba3867f30..e197f77d9 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -61,6 +61,15 @@ def assert_warns_execute_query_experimental(): yield +@contextmanager +def assert_warns_execute_query_bmm_experimental(): + with pytest.warns( + ExperimentalWarning, + match=f"^Driver.query_bookmark_manager is experimental." + ): + yield + + @pytest.mark.parametrize("protocol", ("bolt://", "bolt+s://", "bolt+ssc://")) @pytest.mark.parametrize("host", ("localhost", "127.0.0.1", @@ -718,7 +727,8 @@ def test_execute_query_bookmark_manager( with driver as driver: with assert_warns_execute_query_experimental(): if bookmark_manager is Ellipsis: - bookmark_manager = driver.query_bookmark_manager + with assert_warns_execute_query_bmm_experimental(): + bookmark_manager = driver.query_bookmark_manager driver.execute_query("") else: if positional: @@ -755,9 +765,10 @@ def test_execute_query_result_transformer( else: res_custom: SomeClass if positional: + with assert_warns_execute_query_bmm_experimental(): + bmm = driver.query_bookmark_manager res_custom = driver.execute_query( - "", None, "w", None, None, - driver.query_bookmark_manager, result_transformer + "", None, "w", None, None, bmm, result_transformer ) else: res_custom = driver.execute_query( From fb0f2c398d5e985709bc8f5793a5a9d76d1ed925 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 25 Jan 2023 10:47:15 +0100 Subject: [PATCH 14/15] Emit ExperimentalWarning for Result.to_eager_result --- src/neo4j/_async/driver.py | 6 ++++++ src/neo4j/_async/work/result.py | 4 ++++ src/neo4j/_sync/driver.py | 6 ++++++ src/neo4j/_sync/work/result.py | 4 ++++ tests/unit/async_/test_driver.py | 4 ++-- tests/unit/async_/work/test_result.py | 16 +++++++++++++++- tests/unit/sync/test_driver.py | 4 ++-- tests/unit/sync/work/test_result.py | 16 +++++++++++++++- 8 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index 4dfca6844..06027c01f 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -975,6 +975,12 @@ async def _work( transformer: t.Callable[[AsyncResult], t.Awaitable[_T]] ) -> _T: res = await tx.run(query, parameters) + if transformer is AsyncResult.to_eager_result: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + message=r".*\bto_eager_result\b.*", + category=ExperimentalWarning) + return await transformer(res) return await transformer(res) diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index f70c2b2c3..5d17dd1af 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -604,6 +604,10 @@ async def data(self, *keys: _TResultKey) -> t.List[t.Dict[str, t.Any]]: """ return [record.data(*keys) async for record in self] + @experimental( + "Result.to_eager_result is experimental. " + "It might be changed or removed any time even without prior notice." + ) async def to_eager_result(self) -> EagerResult: """Convert this result to an :class:`.EagerResult`. diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index 06ffd957a..9bc994e55 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -973,6 +973,12 @@ def _work( transformer: t.Callable[[Result], t.Union[_T]] ) -> _T: res = tx.run(query, parameters) + if transformer is Result.to_eager_result: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + message=r".*\bto_eager_result\b.*", + category=ExperimentalWarning) + return transformer(res) return transformer(res) diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index 624acc458..a8a2a2f84 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -604,6 +604,10 @@ def data(self, *keys: _TResultKey) -> t.List[t.Dict[str, t.Any]]: """ return [record.data(*keys) for record in self] + @experimental( + "Result.to_eager_result is experimental. " + "It might be changed or removed any time even without prior notice." + ) def to_eager_result(self) -> EagerResult: """Convert this result to an :class:`.EagerResult`. diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 10ae46018..84558ff2e 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -57,7 +57,7 @@ def assert_warns_execute_query_experimental(): with pytest.warns( ExperimentalWarning, - match=f"^Driver.execute_query is experimental." + match=r"^Driver\.execute_query is experimental\." ): yield @@ -66,7 +66,7 @@ def assert_warns_execute_query_experimental(): def assert_warns_execute_query_bmm_experimental(): with pytest.warns( ExperimentalWarning, - match=f"^Driver.query_bookmark_manager is experimental." + match=r"^Driver\.query_bookmark_manager is experimental\." ): yield diff --git a/tests/unit/async_/work/test_result.py b/tests/unit/async_/work/test_result.py index ca1b11557..40b138414 100644 --- a/tests/unit/async_/work/test_result.py +++ b/tests/unit/async_/work/test_result.py @@ -14,8 +14,11 @@ # 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. + + import uuid import warnings +from contextlib import contextmanager from unittest import mock import pandas as pd @@ -26,6 +29,7 @@ Address, AsyncResult, EagerResult, + ExperimentalWarning, Record, ResultSummary, ServerInfo, @@ -52,6 +56,15 @@ from ...._async_compat import mark_async_test +@contextmanager +def assert_warns_to_eager_result_experimental(): + with pytest.warns( + ExperimentalWarning, + match=r"^Result\.to_eager_result is experimental\." + ): + yield + + class Records: def __init__(self, fields, records): self.fields = tuple(fields) @@ -707,7 +720,8 @@ async def test_to_eager_result(records): connection = AsyncConnectionStub(records=records, summary_meta=summary) result = AsyncResult(connection, 1, noop, noop) await result._run("CYPHER", {}, None, None, "r", None) - eager_result = await result.to_eager_result() + with assert_warns_to_eager_result_experimental(): + eager_result = await result.to_eager_result() assert isinstance(eager_result, EagerResult) diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index e197f77d9..64a55f0b3 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -56,7 +56,7 @@ def assert_warns_execute_query_experimental(): with pytest.warns( ExperimentalWarning, - match=f"^Driver.execute_query is experimental." + match=r"^Driver\.execute_query is experimental\." ): yield @@ -65,7 +65,7 @@ def assert_warns_execute_query_experimental(): def assert_warns_execute_query_bmm_experimental(): with pytest.warns( ExperimentalWarning, - match=f"^Driver.query_bookmark_manager is experimental." + match=r"^Driver\.query_bookmark_manager is experimental\." ): yield diff --git a/tests/unit/sync/work/test_result.py b/tests/unit/sync/work/test_result.py index 21a5f06ea..40077093f 100644 --- a/tests/unit/sync/work/test_result.py +++ b/tests/unit/sync/work/test_result.py @@ -14,8 +14,11 @@ # 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. + + import uuid import warnings +from contextlib import contextmanager from unittest import mock import pandas as pd @@ -25,6 +28,7 @@ from neo4j import ( Address, EagerResult, + ExperimentalWarning, Record, Result, ResultSummary, @@ -52,6 +56,15 @@ from ...._async_compat import mark_sync_test +@contextmanager +def assert_warns_to_eager_result_experimental(): + with pytest.warns( + ExperimentalWarning, + match=r"^Result\.to_eager_result is experimental\." + ): + yield + + class Records: def __init__(self, fields, records): self.fields = tuple(fields) @@ -707,7 +720,8 @@ def test_to_eager_result(records): connection = ConnectionStub(records=records, summary_meta=summary) result = Result(connection, 1, noop, noop) result._run("CYPHER", {}, None, None, "r", None) - eager_result = result.to_eager_result() + with assert_warns_to_eager_result_experimental(): + eager_result = result.to_eager_result() assert isinstance(eager_result, EagerResult) From aa5e7bde057243d82f4be4c42723f07766d38a30 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 25 Jan 2023 10:52:47 +0100 Subject: [PATCH 15/15] Enable type checking for test that includes type annotations --- tests/unit/async_/test_driver.py | 2 +- tests/unit/sync/test_driver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 84558ff2e..1a82a0435 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -435,7 +435,7 @@ class SomeClass: @mark_async_test -async def test_execute_query_work(mocker): +async def test_execute_query_work(mocker) -> None: tx_mock = mocker.AsyncMock(spec=neo4j.AsyncManagedTransaction) transformer_mock = mocker.AsyncMock() transformer: t.Callable[[AsyncResult], t.Awaitable[SomeClass]] = \ diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index 64a55f0b3..29c526f62 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -434,7 +434,7 @@ class SomeClass: @mark_sync_test -def test_execute_query_work(mocker): +def test_execute_query_work(mocker) -> None: tx_mock = mocker.Mock(spec=neo4j.ManagedTransaction) transformer_mock = mocker.Mock() transformer: t.Callable[[Result], t.Union[SomeClass]] = \