diff --git a/CHANGELOG.md b/CHANGELOG.md index f0da3d692..4d971f81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Neo4j Driver Change Log (breaking/major changes only) +## Version 5.3 +- Python 3.11 support added +- Query strings are now typed `LiteralString` instead of `str` to help mitigate + accidental Cypher injections. There are rare use-cases where a computed + string is necessary. Please use `# type: ignore`, or `typing.cast` to + suppress the type checking in those cases. + + ## Version 5.2 - No breaking or major changes. diff --git a/docs/source/index.rst b/docs/source/index.rst index b11caecd6..aaebdce5d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,6 +13,7 @@ Neo4j versions supported: Python versions supported: +* Python 3.11 (added in driver version 5.3.0) * Python 3.10 * Python 3.9 * Python 3.8 diff --git a/neo4j/_async/work/session.py b/neo4j/_async/work/session.py index 36bfe7bef..24bd9d1a5 100644 --- a/neo4j/_async/work/session.py +++ b/neo4j/_async/work/session.py @@ -24,17 +24,6 @@ from random import random from time import perf_counter - -if t.TYPE_CHECKING: - import typing_extensions as te - - from ..io import AsyncBolt - - _R = t.TypeVar("_R") - _P = te.ParamSpec("_P") - - - from ..._async_compat import async_sleep from ..._async_compat.util import AsyncUtil from ..._conf import SessionConfig @@ -61,6 +50,15 @@ from .workspace import AsyncWorkspace +if t.TYPE_CHECKING: + import typing_extensions as te + + from ..io import AsyncBolt + + _R = t.TypeVar("_R") + _P = te.ParamSpec("_P") + + log = getLogger("neo4j") @@ -237,7 +235,7 @@ def cancel(self) -> None: async def run( self, - query: t.Union[str, Query], + query: t.Union[te.LiteralString, Query], parameters: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any ) -> AsyncResult: diff --git a/neo4j/_async/work/transaction.py b/neo4j/_async/work/transaction.py index 7cc071275..40b546323 100644 --- a/neo4j/_async/work/transaction.py +++ b/neo4j/_async/work/transaction.py @@ -29,6 +29,10 @@ from .result import AsyncResult +if t.TYPE_CHECKING: + import typing_extensions as te + + __all__ = ( "AsyncManagedTransaction", "AsyncTransaction", @@ -95,7 +99,7 @@ async def _consume_results(self): async def run( self, - query: str, + query: te.LiteralString, parameters: t.Optional[t.Dict[str, t.Any]] = None, **kwparameters: t.Any ) -> AsyncResult: diff --git a/neo4j/_async_compat/network/_bolt_socket.py b/neo4j/_async_compat/network/_bolt_socket.py index 64b76fb49..768ebe28b 100644 --- a/neo4j/_async_compat/network/_bolt_socket.py +++ b/neo4j/_async_compat/network/_bolt_socket.py @@ -244,7 +244,7 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl): await cls.close_socket(s) raise ServiceUnavailable( "Timed out trying to establish connection to {!r}".format( - resolved_address)) + resolved_address)) from None except asyncio.CancelledError: log.debug("[#0000] S: %s", resolved_address) log.debug("[#0000] C: %s", resolved_address) @@ -259,15 +259,18 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl): message="Failed to establish encrypted connection.", address=(resolved_address.host_name, local_port) ) from error - except OSError as error: + except Exception as error: log.debug("[#0000] S: %s %s", type(error).__name__, " ".join(map(repr, error.args))) log.debug("[#0000] C: %s", resolved_address) if s: await cls.close_socket(s) - raise ServiceUnavailable( - "Failed to establish connection to {!r} (reason {})".format( - resolved_address, error)) + if isinstance(error, OSError): + raise ServiceUnavailable( + "Failed to establish connection to {!r} (reason {})" + .format(resolved_address, error) + ) from error + raise async def _handshake(self, resolved_address): """ @@ -302,10 +305,10 @@ async def _handshake(self, resolved_address): self.settimeout(original_timeout + 1) try: data = await self.recv(4) - except OSError: + except OSError as exc: raise ServiceUnavailable( "Failed to read any data from server {!r} " - "after connected".format(resolved_address)) + "after connected".format(resolved_address)) from exc finally: self.settimeout(original_timeout) data_size = len(data) @@ -513,14 +516,17 @@ def _connect(cls, resolved_address, timeout, keep_alive): raise ServiceUnavailable( "Timed out trying to establish connection to {!r}".format( resolved_address)) - except OSError as error: + except Exception as error: log.debug("[#0000] S: %s %s", type(error).__name__, " ".join(map(repr, error.args))) log.debug("[#0000] C: %s", resolved_address) cls.close_socket(s) - raise ServiceUnavailable( - "Failed to establish connection to {!r} (reason {})".format( - resolved_address, error)) + if isinstance(error, OSError): + raise ServiceUnavailable( + "Failed to establish connection to {!r} (reason {})" + .format(resolved_address, error) + ) from error + raise @classmethod def _secure(cls, s, host, ssl_context): @@ -582,10 +588,10 @@ def _handshake(cls, s, resolved_address): selector.select(1) try: data = s.recv(4) - except OSError: + except OSError as exc: raise ServiceUnavailable( "Failed to read any data from server {!r} " - "after connected".format(resolved_address)) + "after connected".format(resolved_address)) from exc data_size = len(data) if data_size == 0: # If no data is returned after a successful select diff --git a/neo4j/_sync/work/session.py b/neo4j/_sync/work/session.py index e2106c244..f2947a9f7 100644 --- a/neo4j/_sync/work/session.py +++ b/neo4j/_sync/work/session.py @@ -24,17 +24,6 @@ from random import random from time import perf_counter - -if t.TYPE_CHECKING: - import typing_extensions as te - - from ..io import Bolt - - _R = t.TypeVar("_R") - _P = te.ParamSpec("_P") - - - from ..._async_compat import sleep from ..._async_compat.util import Util from ..._conf import SessionConfig @@ -61,6 +50,15 @@ from .workspace import Workspace +if t.TYPE_CHECKING: + import typing_extensions as te + + from ..io import Bolt + + _R = t.TypeVar("_R") + _P = te.ParamSpec("_P") + + log = getLogger("neo4j") @@ -237,7 +235,7 @@ def cancel(self) -> None: def run( self, - query: t.Union[str, Query], + query: t.Union[te.LiteralString, Query], parameters: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any ) -> Result: diff --git a/neo4j/_sync/work/transaction.py b/neo4j/_sync/work/transaction.py index 23d6ee647..fdccbf3d9 100644 --- a/neo4j/_sync/work/transaction.py +++ b/neo4j/_sync/work/transaction.py @@ -29,6 +29,10 @@ from .result import Result +if t.TYPE_CHECKING: + import typing_extensions as te + + __all__ = ( "ManagedTransaction", "Transaction", @@ -95,7 +99,7 @@ def _consume_results(self): def run( self, - query: str, + query: te.LiteralString, parameters: t.Optional[t.Dict[str, t.Any]] = None, **kwparameters: t.Any ) -> Result: diff --git a/neo4j/work/query.py b/neo4j/work/query.py index 1b06687a0..e0682a528 100644 --- a/neo4j/work/query.py +++ b/neo4j/work/query.py @@ -22,6 +22,8 @@ if t.TYPE_CHECKING: + import typing_extensions as te + _T = t.TypeVar("_T") @@ -38,7 +40,7 @@ class Query: """ def __init__( self, - text: str, + text: te.LiteralString, metadata: t.Optional[t.Dict[str, t.Any]] = None, timeout: t.Optional[float] = None ) -> None: @@ -47,7 +49,7 @@ def __init__( self.metadata = metadata self.timeout = timeout - def __str__(self) -> str: + def __str__(self) -> te.LiteralString: return str(self.text) diff --git a/setup.py b/setup.py index 01a570929..34730a56d 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] entry_points = { "console_scripts": [ diff --git a/testkit/Dockerfile b/testkit/Dockerfile index be32548e5..b7a073208 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -42,7 +42,7 @@ ENV PYENV_ROOT /.pyenv ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH # Setup python version -ENV PYTHON_VERSIONS 3.7 3.8 3.9 3.10 +ENV PYTHON_VERSIONS 3.7 3.8 3.9 3.10 3.11 RUN for version in $PYTHON_VERSIONS; do \ pyenv install $version:latest; \ @@ -53,7 +53,7 @@ RUN pyenv global $(pyenv versions --bare --skip-aliases) # Install Latest pip and setuptools for each environment # + tox and tools for starting the tests # https://pip.pypa.io/en/stable/news/ -RUN for version in 3.7 3.8 3.9 3.10; do \ +RUN for version in $PYTHON_VERSIONS; do \ python$version -m pip install -U pip && \ python$version -m pip install -U setuptools && \ python$version -m pip install -U coverage tox tox-factor; \ diff --git a/tox.ini b/tox.ini index b414ff11f..27eb4a4dc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{37,38,39,310}-{unit,integration,performance} +envlist = py{37,38,39,310,311}-{unit,integration,performance} [testenv] passenv =