Skip to content

Commit 12cd301

Browse files
authored
Merge pull request #221 from minrk/validate-kernelspec-name
validate kernelspec names
2 parents f257211 + 578bea4 commit 12cd301

File tree

3 files changed

+57
-4
lines changed

3 files changed

+57
-4
lines changed

docs/kernels.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ locations:
108108
The user location takes priority over the system locations, and the case of the
109109
names is ignored, so selecting kernels works the same way whether or not the
110110
filesystem is case sensitive.
111+
Since kernelspecs show up in URLs and other places,
112+
a kernelspec is required to have a simple name, only containing ASCII letters, ASCII numbers, and the simple separators: ``-`` hyphen, ``.`` period, ``_`` underscore.
111113

112114
Other locations may also be searched if the :envvar:`JUPYTER_PATH` environment
113115
variable is set.

jupyter_client/kernelspec.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io
77
import json
88
import os
9+
import re
910
import shutil
1011
import warnings
1112

@@ -20,6 +21,7 @@
2021

2122
NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2'
2223

24+
2325
class KernelSpec(HasTraits):
2426
argv = List()
2527
display_name = Unicode()
@@ -54,19 +56,44 @@ def to_json(self):
5456
"""
5557
return json.dumps(self.to_dict())
5658

59+
60+
_kernel_name_pat = re.compile(r'^[a-z0-9._\-]+$', re.IGNORECASE)
61+
62+
def _is_valid_kernel_name(name):
63+
"""Check that a kernel name is valid."""
64+
# quote is not unicode-safe on Python 2
65+
return _kernel_name_pat.match(name)
66+
67+
68+
_kernel_name_description = "Kernel names can only contain ASCII letters and numbers and these separators:" \
69+
" - . _ (hyphen, period, and underscore)."
70+
71+
5772
def _is_kernel_dir(path):
5873
"""Is ``path`` a kernel directory?"""
5974
return os.path.isdir(path) and os.path.isfile(pjoin(path, 'kernel.json'))
6075

76+
6177
def _list_kernels_in(dir):
6278
"""Return a mapping of kernel names to resource directories from dir.
6379
6480
If dir is None or does not exist, returns an empty dict.
6581
"""
6682
if dir is None or not os.path.isdir(dir):
6783
return {}
68-
return {f.lower(): pjoin(dir, f) for f in os.listdir(dir)
69-
if _is_kernel_dir(pjoin(dir, f))}
84+
kernels = {}
85+
for f in os.listdir(dir):
86+
path = pjoin(dir, f)
87+
if not _is_kernel_dir(path):
88+
continue
89+
key = f.lower()
90+
if not _is_valid_kernel_name(key):
91+
warnings.warn("Invalid kernelspec directory name (%s): %s",
92+
_kernel_name_description, path, stacklevel=3,
93+
)
94+
kernels[key] = path
95+
return kernels
96+
7097

7198
class NoSuchKernel(KeyError):
7299
def __init__(self, name):
@@ -75,6 +102,7 @@ def __init__(self, name):
75102
def __str__(self):
76103
return "No such kernel named {}".format(self.name)
77104

105+
78106
class KernelSpecManager(LoggingConfigurable):
79107

80108
kernel_spec_class = Type(KernelSpec, config=True,
@@ -242,10 +270,12 @@ def install_kernel_spec(self, source_dir, kernel_name=None, user=False,
242270
if not kernel_name:
243271
kernel_name = os.path.basename(source_dir)
244272
kernel_name = kernel_name.lower()
245-
273+
if not _is_valid_kernel_name(kernel_name):
274+
raise ValueError("Invalid kernel name %r. %s" % (kernel_name, _kernel_name_description))
275+
246276
if user and prefix:
247277
raise ValueError("Can't specify both user and prefix. Please choose one or the other.")
248-
278+
249279
if replace is not None:
250280
warnings.warn(
251281
"replace is ignored. Installing a kernelspec always replaces an existing installation",

jupyter_client/tests/test_kernelspec.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# coding: utf-8
12
"""Tests for the KernelSpecManager"""
23

34
# Copyright (c) Jupyter Development Team.
@@ -141,3 +142,23 @@ def test_remove_kernel_spec_app(self):
141142
)
142143
out, _ = p.communicate()
143144
self.assertEqual(p.returncode, 0, out.decode('utf8', 'replace'))
145+
146+
def test_validate_kernel_name(self):
147+
for good in [
148+
'julia-0.4',
149+
'ipython',
150+
'R',
151+
'python_3',
152+
'Haskell-1-2-3',
153+
]:
154+
assert kernelspec._is_valid_kernel_name(good)
155+
156+
for bad in [
157+
'has space',
158+
u'ünicode',
159+
'%percent',
160+
'question?',
161+
]:
162+
assert not kernelspec._is_valid_kernel_name(bad)
163+
164+

0 commit comments

Comments
 (0)