Skip to content

Commit 7c9f9b4

Browse files
authored
Defer the psutil import until it is actually needed (#1551)
psutil is optional and its import is not cheap, yet it was paid for on every kernel startup even though most sessions never ask for usage information or need to enumerate child processes. It is now imported through `_get_psutil()`, which caches the (possibly None) result on first use.
2 parents a12840a + 41721a1 commit 7c9f9b4

2 files changed

Lines changed: 26 additions & 11 deletions

File tree

ipykernel/kernelbase.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,29 @@
6161
from .iostream import OutStream
6262
from .utils import LazyDict, _async_in_context
6363

64-
psutil: t.Any | None
65-
try:
66-
import psutil as _psutil
67-
except ImportError:
68-
psutil = None
69-
else:
70-
psutil = _psutil
64+
psutil: t.Any | None = None
65+
_NO_SUCH_PROCESS: tuple[type[BaseException], ...] = ()
66+
_psutil_import_attempted = False
67+
68+
69+
def _get_psutil() -> t.Any | None:
70+
"""Import psutil on first use, caching the (possibly None) result.
71+
72+
psutil is optional and its import is not cheap, so we avoid paying for
73+
it unless something actually needs process/resource-usage information.
74+
"""
75+
global psutil, _NO_SUCH_PROCESS, _psutil_import_attempted # noqa: PLW0603
76+
if not _psutil_import_attempted:
77+
_psutil_import_attempted = True
78+
try:
79+
import psutil as _psutil
80+
except ImportError:
81+
pass
82+
else:
83+
psutil = _psutil
84+
_NO_SUCH_PROCESS = (psutil.NoSuchProcess,)
85+
return psutil
7186

72-
if psutil is None:
73-
_NO_SUCH_PROCESS: tuple[type[BaseException], ...] = ()
74-
else:
75-
_NO_SUCH_PROCESS = (psutil.NoSuchProcess,)
7687

7788
_AWAITABLE_MESSAGE: str = (
7889
"For consistency across implementations, it is recommended that `{func_name}`"
@@ -1174,6 +1185,7 @@ async def usage_request(self, stream, ident, parent):
11741185
if not self.session:
11751186
return
11761187
reply_content = {"hostname": socket.gethostname(), "pid": os.getpid()}
1188+
psutil = _get_psutil()
11771189
if psutil is None:
11781190
reply_content["cpu_count"] = os.cpu_count()
11791191
reply_msg = self.session.send(stream, "usage_reply", reply_content, parent, ident)
@@ -1505,6 +1517,7 @@ def _process_children(self):
15051517
- including parents and self with killpg
15061518
- including all children that may have forked-off a new group
15071519
"""
1520+
psutil = _get_psutil()
15081521
if psutil is None:
15091522
return []
15101523

tests/test_kernel_direct.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ async def test_usage_request_without_psutil(kernel, monkeypatch):
157157
import ipykernel.kernelbase as kernelbase
158158

159159
monkeypatch.setattr(kernelbase, "psutil", None)
160+
monkeypatch.setattr(kernelbase, "_psutil_import_attempted", True)
160161
reply = await kernel.test_control_message("usage_request", {})
161162
content = reply["content"]
162163

@@ -173,6 +174,7 @@ async def test_child_process_fallbacks_without_psutil(kernel, monkeypatch):
173174
import ipykernel.kernelbase as kernelbase
174175

175176
monkeypatch.setattr(kernelbase, "psutil", None)
177+
monkeypatch.setattr(kernelbase, "_psutil_import_attempted", True)
176178

177179
assert kernel._process_children() == []
178180
kernel._signal_children(signal.SIGTERM)

0 commit comments

Comments
 (0)