Skip to content

Commit 29f9415

Browse files
authored
Add Python 3.11 support (#861)
* Add Python 3.11 support * Improve error handling in network wrappers
1 parent 01335c7 commit 29f9415

File tree

11 files changed

+66
-44
lines changed

11 files changed

+66
-44
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Neo4j Driver Change Log (breaking/major changes only)
22

3+
## Version 5.3
4+
- Python 3.11 support added
5+
- Query strings are now typed `LiteralString` instead of `str` to help mitigate
6+
accidental Cypher injections. There are rare use-cases where a computed
7+
string is necessary. Please use `# type: ignore`, or `typing.cast` to
8+
suppress the type checking in those cases.
9+
10+
311
## Version 5.2
412

513
- No breaking or major changes.

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Neo4j versions supported:
1313

1414
Python versions supported:
1515

16+
* Python 3.11 (added in driver version 5.3.0)
1617
* Python 3.10
1718
* Python 3.9
1819
* Python 3.8

neo4j/_async/work/session.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@
2424
from random import random
2525
from time import perf_counter
2626

27-
28-
if t.TYPE_CHECKING:
29-
import typing_extensions as te
30-
31-
from ..io import AsyncBolt
32-
33-
_R = t.TypeVar("_R")
34-
_P = te.ParamSpec("_P")
35-
36-
37-
3827
from ..._async_compat import async_sleep
3928
from ..._async_compat.util import AsyncUtil
4029
from ..._conf import SessionConfig
@@ -61,6 +50,15 @@
6150
from .workspace import AsyncWorkspace
6251

6352

53+
if t.TYPE_CHECKING:
54+
import typing_extensions as te
55+
56+
from ..io import AsyncBolt
57+
58+
_R = t.TypeVar("_R")
59+
_P = te.ParamSpec("_P")
60+
61+
6462
log = getLogger("neo4j")
6563

6664

@@ -237,7 +235,7 @@ def cancel(self) -> None:
237235

238236
async def run(
239237
self,
240-
query: t.Union[str, Query],
238+
query: t.Union[te.LiteralString, Query],
241239
parameters: t.Optional[t.Dict[str, t.Any]] = None,
242240
**kwargs: t.Any
243241
) -> AsyncResult:

neo4j/_async/work/transaction.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
from .result import AsyncResult
3030

3131

32+
if t.TYPE_CHECKING:
33+
import typing_extensions as te
34+
35+
3236
__all__ = (
3337
"AsyncManagedTransaction",
3438
"AsyncTransaction",
@@ -95,7 +99,7 @@ async def _consume_results(self):
9599

96100
async def run(
97101
self,
98-
query: str,
102+
query: te.LiteralString,
99103
parameters: t.Optional[t.Dict[str, t.Any]] = None,
100104
**kwparameters: t.Any
101105
) -> AsyncResult:

neo4j/_async_compat/network/_bolt_socket.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl):
244244
await cls.close_socket(s)
245245
raise ServiceUnavailable(
246246
"Timed out trying to establish connection to {!r}".format(
247-
resolved_address))
247+
resolved_address)) from None
248248
except asyncio.CancelledError:
249249
log.debug("[#0000] S: <CANCELLED> %s", resolved_address)
250250
log.debug("[#0000] C: <CLOSE> %s", resolved_address)
@@ -259,15 +259,18 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl):
259259
message="Failed to establish encrypted connection.",
260260
address=(resolved_address.host_name, local_port)
261261
) from error
262-
except OSError as error:
262+
except Exception as error:
263263
log.debug("[#0000] S: <ERROR> %s %s", type(error).__name__,
264264
" ".join(map(repr, error.args)))
265265
log.debug("[#0000] C: <CLOSE> %s", resolved_address)
266266
if s:
267267
await cls.close_socket(s)
268-
raise ServiceUnavailable(
269-
"Failed to establish connection to {!r} (reason {})".format(
270-
resolved_address, error))
268+
if isinstance(error, OSError):
269+
raise ServiceUnavailable(
270+
"Failed to establish connection to {!r} (reason {})"
271+
.format(resolved_address, error)
272+
) from error
273+
raise
271274

272275
async def _handshake(self, resolved_address):
273276
"""
@@ -302,10 +305,10 @@ async def _handshake(self, resolved_address):
302305
self.settimeout(original_timeout + 1)
303306
try:
304307
data = await self.recv(4)
305-
except OSError:
308+
except OSError as exc:
306309
raise ServiceUnavailable(
307310
"Failed to read any data from server {!r} "
308-
"after connected".format(resolved_address))
311+
"after connected".format(resolved_address)) from exc
309312
finally:
310313
self.settimeout(original_timeout)
311314
data_size = len(data)
@@ -513,14 +516,17 @@ def _connect(cls, resolved_address, timeout, keep_alive):
513516
raise ServiceUnavailable(
514517
"Timed out trying to establish connection to {!r}".format(
515518
resolved_address))
516-
except OSError as error:
519+
except Exception as error:
517520
log.debug("[#0000] S: <ERROR> %s %s", type(error).__name__,
518521
" ".join(map(repr, error.args)))
519522
log.debug("[#0000] C: <CLOSE> %s", resolved_address)
520523
cls.close_socket(s)
521-
raise ServiceUnavailable(
522-
"Failed to establish connection to {!r} (reason {})".format(
523-
resolved_address, error))
524+
if isinstance(error, OSError):
525+
raise ServiceUnavailable(
526+
"Failed to establish connection to {!r} (reason {})"
527+
.format(resolved_address, error)
528+
) from error
529+
raise
524530

525531
@classmethod
526532
def _secure(cls, s, host, ssl_context):
@@ -582,10 +588,10 @@ def _handshake(cls, s, resolved_address):
582588
selector.select(1)
583589
try:
584590
data = s.recv(4)
585-
except OSError:
591+
except OSError as exc:
586592
raise ServiceUnavailable(
587593
"Failed to read any data from server {!r} "
588-
"after connected".format(resolved_address))
594+
"after connected".format(resolved_address)) from exc
589595
data_size = len(data)
590596
if data_size == 0:
591597
# If no data is returned after a successful select

neo4j/_sync/work/session.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@
2424
from random import random
2525
from time import perf_counter
2626

27-
28-
if t.TYPE_CHECKING:
29-
import typing_extensions as te
30-
31-
from ..io import Bolt
32-
33-
_R = t.TypeVar("_R")
34-
_P = te.ParamSpec("_P")
35-
36-
37-
3827
from ..._async_compat import sleep
3928
from ..._async_compat.util import Util
4029
from ..._conf import SessionConfig
@@ -61,6 +50,15 @@
6150
from .workspace import Workspace
6251

6352

53+
if t.TYPE_CHECKING:
54+
import typing_extensions as te
55+
56+
from ..io import Bolt
57+
58+
_R = t.TypeVar("_R")
59+
_P = te.ParamSpec("_P")
60+
61+
6462
log = getLogger("neo4j")
6563

6664

@@ -237,7 +235,7 @@ def cancel(self) -> None:
237235

238236
def run(
239237
self,
240-
query: t.Union[str, Query],
238+
query: t.Union[te.LiteralString, Query],
241239
parameters: t.Optional[t.Dict[str, t.Any]] = None,
242240
**kwargs: t.Any
243241
) -> Result:

neo4j/_sync/work/transaction.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
from .result import Result
3030

3131

32+
if t.TYPE_CHECKING:
33+
import typing_extensions as te
34+
35+
3236
__all__ = (
3337
"ManagedTransaction",
3438
"Transaction",
@@ -95,7 +99,7 @@ def _consume_results(self):
9599

96100
def run(
97101
self,
98-
query: str,
102+
query: te.LiteralString,
99103
parameters: t.Optional[t.Dict[str, t.Any]] = None,
100104
**kwparameters: t.Any
101105
) -> Result:

neo4j/work/query.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323

2424
if t.TYPE_CHECKING:
25+
import typing_extensions as te
26+
2527
_T = t.TypeVar("_T")
2628

2729

@@ -38,7 +40,7 @@ class Query:
3840
"""
3941
def __init__(
4042
self,
41-
text: str,
43+
text: te.LiteralString,
4244
metadata: t.Optional[t.Dict[str, t.Any]] = None,
4345
timeout: t.Optional[float] = None
4446
) -> None:
@@ -47,7 +49,7 @@ def __init__(
4749
self.metadata = metadata
4850
self.timeout = timeout
4951

50-
def __str__(self) -> str:
52+
def __str__(self) -> te.LiteralString:
5153
return str(self.text)
5254

5355

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"Programming Language :: Python :: 3.8",
4848
"Programming Language :: Python :: 3.9",
4949
"Programming Language :: Python :: 3.10",
50+
"Programming Language :: Python :: 3.11",
5051
]
5152
entry_points = {
5253
"console_scripts": [

testkit/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ENV PYENV_ROOT /.pyenv
4242
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
4343

4444
# Setup python version
45-
ENV PYTHON_VERSIONS 3.7 3.8 3.9 3.10
45+
ENV PYTHON_VERSIONS 3.7 3.8 3.9 3.10 3.11
4646

4747
RUN for version in $PYTHON_VERSIONS; do \
4848
pyenv install $version:latest; \
@@ -53,7 +53,7 @@ RUN pyenv global $(pyenv versions --bare --skip-aliases)
5353
# Install Latest pip and setuptools for each environment
5454
# + tox and tools for starting the tests
5555
# https://pip.pypa.io/en/stable/news/
56-
RUN for version in 3.7 3.8 3.9 3.10; do \
56+
RUN for version in $PYTHON_VERSIONS; do \
5757
python$version -m pip install -U pip && \
5858
python$version -m pip install -U setuptools && \
5959
python$version -m pip install -U coverage tox tox-factor; \

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py{37,38,39,310}-{unit,integration,performance}
2+
envlist = py{37,38,39,310,311}-{unit,integration,performance}
33

44
[testenv]
55
passenv =

0 commit comments

Comments
 (0)