Skip to content

Commit 1e5c7f9

Browse files
authored
Avoid unclean interpreter shutdown (#942)
When not closing the driver before shutting down the interpreter, the GC might call `__del__` on the driver, which currently (will be changed with 6.0) closes the driver. So any code that's run on driver closure might be execute just before the interpreter shuts down. This can lead to funky errors when trying to import things dynamically. So this should be avoided. This got raised here: https://community.neo4j.com/t/error-with-the-neo4j-python-driver-importerror-sys-meta-path-is-none-python-is-likely-shutting-down/63096/
1 parent d5abf41 commit 1e5c7f9

File tree

12 files changed

+35
-25
lines changed

12 files changed

+35
-25
lines changed

src/neo4j/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ def __getattr__(name):
176176
if name in (
177177
"log", "Config", "PoolConfig", "SessionConfig", "WorkspaceConfig"
178178
):
179-
from ._meta import deprecation_warn
180-
deprecation_warn(
179+
_deprecation_warn(
181180
"Importing {} from neo4j is deprecated without replacement. It's "
182181
"internal and will be removed in a future version."
183182
.format(name),

src/neo4j/_async/io/_bolt.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from ..._meta import USER_AGENT
3838
from ...addressing import ResolvedAddress
3939
from ...api import (
40+
Auth,
4041
ServerInfo,
4142
Version,
4243
)
@@ -178,7 +179,6 @@ def _to_auth_dict(cls, auth):
178179
if not auth:
179180
return {}
180181
elif isinstance(auth, tuple) and 2 <= len(auth) <= 3:
181-
from ...api import Auth
182182
return vars(Auth("basic", *auth))
183183
else:
184184
try:
@@ -837,8 +837,7 @@ async def _set_defunct_write(self, error=None, silent=False):
837837
await self._set_defunct(message, error=error, silent=silent)
838838

839839
async def _set_defunct(self, message, error=None, silent=False):
840-
from ._pool import AsyncBoltPool
841-
direct_driver = isinstance(self.pool, AsyncBoltPool)
840+
direct_driver = getattr(self.pool, "is_direct_pool", False)
842841
user_cancelled = isinstance(error, asyncio.CancelledError)
843842

844843
if error:

src/neo4j/_async/io/_pool.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ def __init__(self, opener, pool_config, workspace_config):
100100
self.lock = AsyncCooperativeRLock()
101101
self.cond = AsyncCondition(self.lock)
102102

103+
@property
104+
@abc.abstractmethod
105+
def is_direct_pool(self) -> bool:
106+
...
107+
103108
async def __aenter__(self):
104109
return self
105110

@@ -490,6 +495,8 @@ async def close(self):
490495

491496
class AsyncBoltPool(AsyncIOPool):
492497

498+
is_direct_pool = True
499+
493500
@classmethod
494501
def open(cls, address, *, pool_config, workspace_config):
495502
"""Create a new BoltPool
@@ -536,6 +543,8 @@ class AsyncNeo4jPool(AsyncIOPool):
536543
""" Connection pool with routing table.
537544
"""
538545

546+
is_direct_pool = False
547+
539548
@classmethod
540549
def open(cls, *addresses, pool_config, workspace_config,
541550
routing_context=None):
@@ -578,6 +587,7 @@ def __init__(self, opener, pool_config, workspace_config, address):
578587
self.address = address
579588
self.routing_tables = {}
580589
self.refresh_lock = AsyncRLock()
590+
self.is_direct_pool = False
581591

582592
def __repr__(self):
583593
""" The representation shows the initial routing addresses.

src/neo4j/_sync/io/_bolt.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from ..._meta import USER_AGENT
3838
from ...addressing import ResolvedAddress
3939
from ...api import (
40+
Auth,
4041
ServerInfo,
4142
Version,
4243
)
@@ -178,7 +179,6 @@ def _to_auth_dict(cls, auth):
178179
if not auth:
179180
return {}
180181
elif isinstance(auth, tuple) and 2 <= len(auth) <= 3:
181-
from ...api import Auth
182182
return vars(Auth("basic", *auth))
183183
else:
184184
try:
@@ -837,8 +837,7 @@ def _set_defunct_write(self, error=None, silent=False):
837837
self._set_defunct(message, error=error, silent=silent)
838838

839839
def _set_defunct(self, message, error=None, silent=False):
840-
from ._pool import BoltPool
841-
direct_driver = isinstance(self.pool, BoltPool)
840+
direct_driver = getattr(self.pool, "is_direct_pool", False)
842841
user_cancelled = isinstance(error, asyncio.CancelledError)
843842

844843
if error:

src/neo4j/_sync/io/_pool.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ def __init__(self, opener, pool_config, workspace_config):
9797
self.lock = CooperativeRLock()
9898
self.cond = Condition(self.lock)
9999

100+
@property
101+
@abc.abstractmethod
102+
def is_direct_pool(self) -> bool:
103+
...
104+
100105
def __enter__(self):
101106
return self
102107

@@ -487,6 +492,8 @@ def close(self):
487492

488493
class BoltPool(IOPool):
489494

495+
is_direct_pool = True
496+
490497
@classmethod
491498
def open(cls, address, *, pool_config, workspace_config):
492499
"""Create a new BoltPool
@@ -533,6 +540,8 @@ class Neo4jPool(IOPool):
533540
""" Connection pool with routing table.
534541
"""
535542

543+
is_direct_pool = False
544+
536545
@classmethod
537546
def open(cls, *addresses, pool_config, workspace_config,
538547
routing_context=None):
@@ -575,6 +584,7 @@ def __init__(self, opener, pool_config, workspace_config, address):
575584
self.address = address
576585
self.routing_tables = {}
577586
self.refresh_lock = RLock()
587+
self.is_direct_pool = False
578588

579589
def __repr__(self):
580590
""" The representation shows the initial routing addresses.

src/neo4j/time/_clock_implementations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
Structure,
2525
)
2626
from platform import uname
27+
from time import time
2728

2829
from . import (
2930
Clock,
@@ -53,7 +54,6 @@ def available(cls):
5354
return True
5455

5556
def utc_time(self):
56-
from time import time
5757
seconds, nanoseconds = nano_divmod(int(time() * 1000000), 1000000)
5858
return ClockTime(seconds, nanoseconds * 1000)
5959

testkitbackend/_async/requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import datetime
2020
import json
2121
import re
22+
import ssl
2223
import warnings
2324
from os import path
2425

@@ -56,7 +57,6 @@ def load_config():
5657
config = json.load(fd)
5758
skips = config["skips"]
5859
features = [k for k, v in config["features"].items() if v is True]
59-
import ssl
6060
if ssl.HAS_TLSv1_3:
6161
features += ["Feature:TLS:1.3"]
6262
return skips, features

testkitbackend/_sync/requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import datetime
2020
import json
2121
import re
22+
import ssl
2223
import warnings
2324
from os import path
2425

@@ -56,7 +57,6 @@ def load_config():
5657
config = json.load(fd)
5758
skips = config["skips"]
5859
features = [k for k, v in config["features"].items() if v is True]
59-
import ssl
6060
if ssl.HAS_TLSv1_3:
6161
features += ["Feature:TLS:1.3"]
6262
return skips, features

tests/conftest.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,17 @@
1717

1818

1919
import asyncio
20-
import warnings
20+
import sys
2121
from functools import wraps
22-
from os import environ
2322

2423
import pytest
2524
import pytest_asyncio
2625

2726
from neo4j import (
2827
AsyncGraphDatabase,
29-
ExperimentalWarning,
3028
GraphDatabase,
3129
)
32-
from neo4j._exceptions import BoltHandshakeError
33-
from neo4j._sync.io import Bolt
34-
from neo4j.exceptions import ServiceUnavailable
30+
from neo4j.debug import watch
3531

3632
from . import env
3733

@@ -189,8 +185,5 @@ def _():
189185

190186
@pytest.fixture
191187
def watcher():
192-
import sys
193-
194-
from neo4j.debug import watch
195188
with watch("neo4j", out=sys.stdout, colour=True):
196189
yield

tests/unit/async_/io/test_direct.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def timedout(self):
8585

8686

8787
class AsyncFakeBoltPool(AsyncIOPool):
88+
is_direct_pool = False
89+
8890
def __init__(self, address, *, auth=None, **config):
8991
config["auth"] = static_auth(None)
9092
self.pool_config, self.workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)

tests/unit/common/test_api.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import pytest
2525

2626
import neo4j.api
27+
from neo4j.addressing import Address
2728
from neo4j.exceptions import ConfigurationError
2829

2930

@@ -276,9 +277,6 @@ def test_version_to_bytes_with_valid_bolt_version(
276277

277278

278279
def test_serverinfo_initialization() -> None:
279-
280-
from neo4j.addressing import Address
281-
282280
address = Address(("bolt://localhost", 7687))
283281
version = neo4j.Version(3, 0)
284282

@@ -301,8 +299,6 @@ def test_serverinfo_initialization() -> None:
301299
def test_serverinfo_with_metadata(
302300
test_input, expected_agent, protocol_version
303301
) -> None:
304-
from neo4j.addressing import Address
305-
306302
address = Address(("bolt://localhost", 7687))
307303
version = neo4j.Version(*protocol_version)
308304

tests/unit/sync/io/test_direct.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def timedout(self):
8585

8686

8787
class FakeBoltPool(IOPool):
88+
is_direct_pool = False
89+
8890
def __init__(self, address, *, auth=None, **config):
8991
config["auth"] = static_auth(None)
9092
self.pool_config, self.workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)

0 commit comments

Comments
 (0)