Skip to content

Commit eddd3e6

Browse files
authored
Disable frozen modules by default, add a toggle (#1213)
1 parent c6d5ad6 commit eddd3e6

File tree

2 files changed

+92
-13
lines changed

2 files changed

+92
-13
lines changed

ipykernel/kernelspec.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import errno
99
import json
1010
import os
11+
import platform
1112
import shutil
1213
import stat
1314
import sys
@@ -19,11 +20,6 @@
1920
from traitlets import Unicode
2021
from traitlets.config import Application
2122

22-
try:
23-
from .debugger import _is_debugpy_available
24-
except ImportError:
25-
_is_debugpy_available = False
26-
2723
pjoin = os.path.join
2824

2925
KERNEL_NAME = "python%i" % sys.version_info[0]
@@ -36,6 +32,7 @@ def make_ipkernel_cmd(
3632
mod: str = "ipykernel_launcher",
3733
executable: str | None = None,
3834
extra_arguments: list[str] | None = None,
35+
python_arguments: list[str] | None = None,
3936
) -> list[str]:
4037
"""Build Popen command list for launching an IPython kernel.
4138
@@ -55,16 +52,18 @@ def make_ipkernel_cmd(
5552
if executable is None:
5653
executable = sys.executable
5754
extra_arguments = extra_arguments or []
58-
arguments = [executable, "-m", mod, "-f", "{connection_file}"]
59-
arguments.extend(extra_arguments)
60-
61-
return arguments
55+
python_arguments = python_arguments or []
56+
return [executable, *python_arguments, "-m", mod, "-f", "{connection_file}", *extra_arguments]
6257

6358

64-
def get_kernel_dict(extra_arguments: list[str] | None = None) -> dict[str, Any]:
59+
def get_kernel_dict(
60+
extra_arguments: list[str] | None = None, python_arguments: list[str] | None = None
61+
) -> dict[str, Any]:
6562
"""Construct dict for kernel.json"""
6663
return {
67-
"argv": make_ipkernel_cmd(extra_arguments=extra_arguments),
64+
"argv": make_ipkernel_cmd(
65+
extra_arguments=extra_arguments, python_arguments=python_arguments
66+
),
6867
"display_name": "Python %i (ipykernel)" % sys.version_info[0],
6968
"language": "python",
7069
"metadata": {"debugger": True},
@@ -75,6 +74,7 @@ def write_kernel_spec(
7574
path: Path | str | None = None,
7675
overrides: dict[str, Any] | None = None,
7776
extra_arguments: list[str] | None = None,
77+
python_arguments: list[str] | None = None,
7878
) -> str:
7979
"""Write a kernel spec directory to `path`
8080
@@ -95,7 +95,7 @@ def write_kernel_spec(
9595
Path(path).chmod(mask | stat.S_IWUSR)
9696

9797
# write kernel.json
98-
kernel_dict = get_kernel_dict(extra_arguments)
98+
kernel_dict = get_kernel_dict(extra_arguments, python_arguments)
9999

100100
if overrides:
101101
kernel_dict.update(overrides)
@@ -113,6 +113,7 @@ def install(
113113
prefix: str | None = None,
114114
profile: str | None = None,
115115
env: dict[str, str] | None = None,
116+
frozen_modules: bool = False,
116117
) -> str:
117118
"""Install the IPython kernelspec for Jupyter
118119
@@ -137,6 +138,12 @@ def install(
137138
A dictionary of extra environment variables for the kernel.
138139
These will be added to the current environment variables before the
139140
kernel is started
141+
frozen_modules : bool, optional
142+
Whether to use frozen modules for potentially faster kernel startup.
143+
Using frozen modules prevents debugging inside of some built-in
144+
Python modules, such as io, abc, posixpath, ntpath, or stat.
145+
The frozen modules are used in CPython for faster interpreter startup.
146+
Ignored for cPython <3.11 and for other Python implementations.
140147
141148
Returns
142149
-------
@@ -145,6 +152,9 @@ def install(
145152
if kernel_spec_manager is None:
146153
kernel_spec_manager = KernelSpecManager()
147154

155+
if env is None:
156+
env = {}
157+
148158
if (kernel_name != KERNEL_NAME) and (display_name is None):
149159
# kernel_name is specified and display_name is not
150160
# default display_name to kernel_name
@@ -159,9 +169,24 @@ def install(
159169
overrides["display_name"] = "Python %i [profile=%s]" % (sys.version_info[0], profile)
160170
else:
161171
extra_arguments = None
172+
173+
python_arguments = None
174+
175+
# addresses the debugger warning from debugpy about frozen modules
176+
if sys.version_info >= (3, 11) and platform.python_implementation() == "CPython":
177+
if not frozen_modules:
178+
# disable frozen modules
179+
python_arguments = ["-Xfrozen_modules=off"]
180+
elif "PYDEVD_DISABLE_FILE_VALIDATION" not in env:
181+
# user opted-in to have frozen modules, and we warned them about
182+
# consequences for the - disable the debugger warning
183+
env["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
184+
162185
if env:
163186
overrides["env"] = env
164-
path = write_kernel_spec(overrides=overrides, extra_arguments=extra_arguments)
187+
path = write_kernel_spec(
188+
overrides=overrides, extra_arguments=extra_arguments, python_arguments=python_arguments
189+
)
165190
dest = kernel_spec_manager.install_kernel_spec(
166191
path, kernel_name=kernel_name, user=user, prefix=prefix
167192
)
@@ -236,6 +261,12 @@ def start(self) -> None:
236261
metavar=("ENV", "VALUE"),
237262
help="Set environment variables for the kernel.",
238263
)
264+
parser.add_argument(
265+
"--frozen_modules",
266+
action="store_true",
267+
help="Enable frozen modules for potentially faster startup."
268+
" This has a downside of preventing the debugger from navigating to certain built-in modules.",
269+
)
239270
opts = parser.parse_args(self.argv)
240271
if opts.env:
241272
opts.env = dict(opts.env)

tests/test_kernelspec.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import json
55
import os
6+
import platform
67
import shutil
78
import sys
89
import tempfile
@@ -22,6 +23,7 @@
2223
)
2324

2425
pjoin = os.path.join
26+
is_cpython = platform.python_implementation() == "CPython"
2527

2628

2729
def test_make_ipkernel_cmd():
@@ -144,3 +146,49 @@ def test_install_env(tmp_path, env):
144146
assert spec["env"][k] == v
145147
else:
146148
assert "env" not in spec
149+
150+
151+
@pytest.mark.skipif(sys.version_info < (3, 11) or not is_cpython, reason="requires cPython 3.11")
152+
def test_install_frozen_modules_on():
153+
system_jupyter_dir = tempfile.mkdtemp()
154+
155+
with mock.patch("jupyter_client.kernelspec.SYSTEM_JUPYTER_PATH", [system_jupyter_dir]):
156+
install(frozen_modules=True)
157+
158+
spec_file = os.path.join(system_jupyter_dir, "kernels", KERNEL_NAME, "kernel.json")
159+
with open(spec_file) as f:
160+
spec = json.load(f)
161+
assert spec["env"]["PYDEVD_DISABLE_FILE_VALIDATION"] == "1"
162+
assert "-Xfrozen_modules=off" not in spec["argv"]
163+
164+
165+
@pytest.mark.skipif(sys.version_info < (3, 11) or not is_cpython, reason="requires cPython 3.11")
166+
def test_install_frozen_modules_off():
167+
system_jupyter_dir = tempfile.mkdtemp()
168+
169+
with mock.patch("jupyter_client.kernelspec.SYSTEM_JUPYTER_PATH", [system_jupyter_dir]):
170+
install(frozen_modules=False)
171+
172+
spec_file = os.path.join(system_jupyter_dir, "kernels", KERNEL_NAME, "kernel.json")
173+
with open(spec_file) as f:
174+
spec = json.load(f)
175+
assert "env" not in spec
176+
assert spec["argv"][1] == "-Xfrozen_modules=off"
177+
178+
179+
@pytest.mark.skipif(
180+
sys.version_info >= (3, 11) or is_cpython,
181+
reason="checks versions older than 3.11 and other Python implementations",
182+
)
183+
def test_install_frozen_modules_no_op():
184+
# ensure we do not add add Xfrozen_modules on older Python versions
185+
# (although cPython does not error out on unknown X options as of 3.8)
186+
system_jupyter_dir = tempfile.mkdtemp()
187+
188+
with mock.patch("jupyter_client.kernelspec.SYSTEM_JUPYTER_PATH", [system_jupyter_dir]):
189+
install(frozen_modules=False)
190+
191+
spec_file = os.path.join(system_jupyter_dir, "kernels", KERNEL_NAME, "kernel.json")
192+
with open(spec_file) as f:
193+
spec = json.load(f)
194+
assert "-Xfrozen_modules=off" not in spec["argv"]

0 commit comments

Comments
 (0)