Skip to content

Commit 12ffd00

Browse files
authored
Bolt Handshake Manifest v1 (#1102)
* Refactor bolt socket Move handshake and connect method that can be automatically un-async-ed for less code duplication. * minor tox config improvements: enable warning as error on py3.13, too * Drop (undocumented) support for Bolt 4.1 * Tests: fix event loop shutdown for Python <= 3.8
1 parent bd96531 commit 12ffd00

25 files changed

+1061
-803
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.
55
## NEXT RELEASE
66
- Since the types of `Relationship`s are tied to the `Graph` object they belong to, fixing `pickle` support for graph types means that `Relationship`s with the same name will have a different type after `deepcopy`ing or pickling and unpickling them or their graph.
77
For more details, see https://github.com/neo4j/neo4j-python-driver/pull/1133
8+
- Drop undocumented support for Bolt protocol versions 4.1.
89

910

1011
## Version 5.27

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ use_parentheses = true
122122

123123
[tool.pytest.ini_options]
124124
mock_use_standalone_module = true
125-
asyncio_mode = "auto"
125+
asyncio_mode = "strict"
126126

127127

128128
[tool.mypy]

src/neo4j/_async/io/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
]
3333

3434

35+
# [bolt-version-bump] search tag when changing bolt version support
36+
from . import ( # noqa - imports needed to register protocol handlers
37+
_bolt3,
38+
_bolt4,
39+
_bolt5,
40+
)
3541
from ._bolt import AsyncBolt
3642
from ._common import (
3743
check_supported_server_product,

src/neo4j/_async/io/_bolt.py

Lines changed: 38 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from logging import getLogger
2424
from time import monotonic
2525

26-
from ..._async_compat.network import AsyncBoltSocket
2726
from ..._async_compat.util import AsyncUtil
2827
from ..._auth_management import to_auth_dict
2928
from ..._codec.hydration import (
@@ -51,6 +50,7 @@
5150
SessionExpired,
5251
)
5352
from ..config import AsyncPoolConfig
53+
from ._bolt_socket import AsyncBoltSocket
5454
from ._common import (
5555
AsyncInbox,
5656
AsyncOutbox,
@@ -59,6 +59,8 @@
5959

6060

6161
if t.TYPE_CHECKING:
62+
import typing_extensions as te
63+
6264
from ..._api import TelemetryAPI
6365

6466

@@ -134,6 +136,8 @@ class AsyncBolt:
134136
# results for it.
135137
most_recent_qid = None
136138

139+
SKIP_REGISTRATION = False
140+
137141
def __init__(
138142
self,
139143
unresolved_address,
@@ -257,101 +261,36 @@ def assert_notification_filtering_support(self):
257261
f"{self.server_info.agent!r}"
258262
)
259263

260-
# [bolt-version-bump] search tag when changing bolt version support
261-
@classmethod
262-
def protocol_handlers(cls, protocol_version=None):
263-
"""
264-
Return a dictionary of available Bolt protocol handlers.
265-
266-
The handlers are keyed by version tuple. If an explicit protocol
267-
version is provided, the dictionary will contain either zero or one
268-
items, depending on whether that version is supported. If no protocol
269-
version is provided, all available versions will be returned.
270-
271-
:param protocol_version: tuple identifying a specific protocol
272-
version (e.g. (3, 5)) or None
273-
:returns: dictionary of version tuple to handler class for all
274-
relevant and supported protocol versions
275-
:raise TypeError: if protocol version is not passed in a tuple
276-
"""
277-
# Carry out Bolt subclass imports locally to avoid circular dependency
278-
# issues.
279-
from ._bolt3 import AsyncBolt3
280-
from ._bolt4 import (
281-
AsyncBolt4x1,
282-
AsyncBolt4x2,
283-
AsyncBolt4x3,
284-
AsyncBolt4x4,
285-
)
286-
from ._bolt5 import (
287-
AsyncBolt5x0,
288-
AsyncBolt5x1,
289-
AsyncBolt5x2,
290-
AsyncBolt5x3,
291-
AsyncBolt5x4,
292-
AsyncBolt5x5,
293-
AsyncBolt5x6,
294-
AsyncBolt5x7,
295-
AsyncBolt5x8,
296-
)
297-
298-
handlers = {
299-
AsyncBolt3.PROTOCOL_VERSION: AsyncBolt3,
300-
# 4.0 unsupported because no space left in the handshake
301-
AsyncBolt4x1.PROTOCOL_VERSION: AsyncBolt4x1,
302-
AsyncBolt4x2.PROTOCOL_VERSION: AsyncBolt4x2,
303-
AsyncBolt4x3.PROTOCOL_VERSION: AsyncBolt4x3,
304-
AsyncBolt4x4.PROTOCOL_VERSION: AsyncBolt4x4,
305-
AsyncBolt5x0.PROTOCOL_VERSION: AsyncBolt5x0,
306-
AsyncBolt5x1.PROTOCOL_VERSION: AsyncBolt5x1,
307-
AsyncBolt5x2.PROTOCOL_VERSION: AsyncBolt5x2,
308-
AsyncBolt5x3.PROTOCOL_VERSION: AsyncBolt5x3,
309-
AsyncBolt5x4.PROTOCOL_VERSION: AsyncBolt5x4,
310-
AsyncBolt5x5.PROTOCOL_VERSION: AsyncBolt5x5,
311-
AsyncBolt5x6.PROTOCOL_VERSION: AsyncBolt5x6,
312-
AsyncBolt5x7.PROTOCOL_VERSION: AsyncBolt5x7,
313-
AsyncBolt5x8.PROTOCOL_VERSION: AsyncBolt5x8,
314-
}
264+
protocol_handlers: t.ClassVar[dict[Version, type[AsyncBolt]]] = {}
315265

266+
def __init_subclass__(cls: type[te.Self], **kwargs: t.Any) -> None:
267+
if cls.SKIP_REGISTRATION:
268+
super().__init_subclass__(**kwargs)
269+
return
270+
protocol_version = cls.PROTOCOL_VERSION
316271
if protocol_version is None:
317-
return handlers
318-
319-
if not isinstance(protocol_version, tuple):
320-
raise TypeError("Protocol version must be specified as a tuple")
321-
322-
if protocol_version in handlers:
323-
return {protocol_version: handlers[protocol_version]}
324-
325-
return {}
326-
327-
@classmethod
328-
def version_list(cls, versions):
329-
"""
330-
Return a list of supported protocol versions in order of preference.
331-
332-
The number of protocol versions (or ranges) returned is limited to 4.
333-
"""
334-
# In fact, 4.3 is the fist version to support ranges. However, the
335-
# range support got backported to 4.2. But even if the server is too
336-
# old to have the backport, negotiating BOLT 4.1 is no problem as it's
337-
# equivalent to 4.2
338-
first_with_range_support = Version(4, 2)
339-
result = []
340-
for version in versions:
341-
if (
342-
result
343-
and version >= first_with_range_support
344-
and result[-1][0] == version[0]
345-
and result[-1][1][1] == version[1] + 1
346-
):
347-
# can use range to encompass this version
348-
result[-1][1][1] = version[1]
349-
continue
350-
result.append(Version(version[0], [version[1], version[1]]))
351-
if len(result) == 4:
352-
break
353-
return result
272+
raise ValueError(
273+
"AsyncBolt subclasses must define PROTOCOL_VERSION"
274+
)
275+
if not (
276+
isinstance(protocol_version, Version)
277+
and len(protocol_version) == 2
278+
and all(isinstance(i, int) for i in protocol_version)
279+
):
280+
raise TypeError(
281+
"PROTOCOL_VERSION must be a 2-tuple of integers, not "
282+
f"{protocol_version!r}"
283+
)
284+
if protocol_version in AsyncBolt.protocol_handlers:
285+
cls_conflict = AsyncBolt.protocol_handlers[protocol_version]
286+
raise TypeError(
287+
f"Multiple classes for the same protocol version "
288+
f"{protocol_version}: {cls}, {cls_conflict}"
289+
)
290+
cls.protocol_handlers[protocol_version] = cls
291+
super().__init_subclass__(**kwargs)
354292

293+
# [bolt-version-bump] search tag when changing bolt version support
355294
@classmethod
356295
def get_handshake(cls):
357296
"""
@@ -360,12 +299,9 @@ def get_handshake(cls):
360299
The length is 16 bytes as specified in the Bolt version negotiation.
361300
:returns: bytes
362301
"""
363-
supported_versions = sorted(
364-
cls.protocol_handlers().keys(), reverse=True
302+
return (
303+
b"\x00\x00\x01\xff\x00\x08\x08\x05\x00\x02\x04\x04\x00\x00\x00\x03"
365304
)
366-
offered_versions = cls.version_list(supported_versions)
367-
versions_bytes = (v.to_bytes() for v in offered_versions)
368-
return b"".join(versions_bytes).ljust(16, b"\x00")
369305

370306
@classmethod
371307
async def ping(cls, address, *, deadline=None, pool_config=None):
@@ -400,7 +336,6 @@ async def ping(cls, address, *, deadline=None, pool_config=None):
400336
await AsyncBoltSocket.close_socket(s)
401337
return protocol_version
402338

403-
# [bolt-version-bump] search tag when changing bolt version support
404339
@classmethod
405340
async def open(
406341
cls,
@@ -441,71 +376,17 @@ async def open(
441376
)
442377

443378
pool_config.protocol_version = protocol_version
444-
445-
# Carry out Bolt subclass imports locally to avoid circular dependency
446-
# issues.
447-
448-
# avoid new lines after imports for better readability and conciseness
449-
# fmt: off
450-
if protocol_version == (5, 8):
451-
from ._bolt5 import AsyncBolt5x8
452-
bolt_cls = AsyncBolt5x8
453-
elif protocol_version == (5, 7):
454-
from ._bolt5 import AsyncBolt5x7
455-
bolt_cls = AsyncBolt5x7
456-
elif protocol_version == (5, 6):
457-
from ._bolt5 import AsyncBolt5x6
458-
bolt_cls = AsyncBolt5x6
459-
elif protocol_version == (5, 5):
460-
from ._bolt5 import AsyncBolt5x5
461-
bolt_cls = AsyncBolt5x5
462-
elif protocol_version == (5, 4):
463-
from ._bolt5 import AsyncBolt5x4
464-
bolt_cls = AsyncBolt5x4
465-
elif protocol_version == (5, 3):
466-
from ._bolt5 import AsyncBolt5x3
467-
bolt_cls = AsyncBolt5x3
468-
elif protocol_version == (5, 2):
469-
from ._bolt5 import AsyncBolt5x2
470-
bolt_cls = AsyncBolt5x2
471-
elif protocol_version == (5, 1):
472-
from ._bolt5 import AsyncBolt5x1
473-
bolt_cls = AsyncBolt5x1
474-
elif protocol_version == (5, 0):
475-
from ._bolt5 import AsyncBolt5x0
476-
bolt_cls = AsyncBolt5x0
477-
elif protocol_version == (4, 4):
478-
from ._bolt4 import AsyncBolt4x4
479-
bolt_cls = AsyncBolt4x4
480-
elif protocol_version == (4, 3):
481-
from ._bolt4 import AsyncBolt4x3
482-
bolt_cls = AsyncBolt4x3
483-
elif protocol_version == (4, 2):
484-
from ._bolt4 import AsyncBolt4x2
485-
bolt_cls = AsyncBolt4x2
486-
elif protocol_version == (4, 1):
487-
from ._bolt4 import AsyncBolt4x1
488-
bolt_cls = AsyncBolt4x1
489-
# Implementation for 4.0 exists, but there was no space left in the
490-
# handshake to offer this version to the server. Hence, the server
491-
# should never request us to speak bolt 4.0.
492-
# elif protocol_version == (4, 0):
493-
# from ._bolt4 import AsyncBolt4x0
494-
# bolt_cls = AsyncBolt4x0
495-
elif protocol_version == (3, 0):
496-
from ._bolt3 import AsyncBolt3
497-
bolt_cls = AsyncBolt3
498-
# fmt: on
499-
else:
379+
protocol_handlers = AsyncBolt.protocol_handlers
380+
bolt_cls = protocol_handlers.get(protocol_version)
381+
if bolt_cls is None:
500382
log.debug("[#%04X] C: <CLOSE>", s.getsockname()[1])
501383
await AsyncBoltSocket.close_socket(s)
502384

503-
supported_versions = cls.protocol_handlers().keys()
504385
# TODO: 6.0 - raise public DriverError subclass instead
505386
raise BoltHandshakeError(
506387
"The neo4j server does not support communication with this "
507388
"driver. This driver has support for Bolt protocols "
508-
f"{tuple(map(str, supported_versions))}.",
389+
f"{tuple(map(str, AsyncBolt.protocol_handlers.keys()))}.",
509390
address=address,
510391
request_data=handshake,
511392
response_data=data,

src/neo4j/_async/io/_bolt4.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ class AsyncBolt4x0(AsyncBolt):
7474

7575
supports_notification_filtering = False
7676

77+
SKIP_REGISTRATION = True
78+
7779
def __init__(self, *args, **kwargs):
7880
super().__init__(*args, **kwargs)
7981
self._server_state_manager = ServerStateManager(
@@ -550,6 +552,8 @@ class AsyncBolt4x2(AsyncBolt4x1):
550552

551553
PROTOCOL_VERSION = Version(4, 2)
552554

555+
SKIP_REGISTRATION = False
556+
553557

554558
class AsyncBolt4x3(AsyncBolt4x2):
555559
"""

0 commit comments

Comments
 (0)