Skip to content

Commit fe30ee1

Browse files
committed
Add support for templated env entries
This change tolerates the existence of templated entries where the templated value corresponds to an existing environment variable. In such cases, those templated values will be substituted prior to the kernel's launch.
1 parent 2a43817 commit fe30ee1

File tree

4 files changed

+83
-6
lines changed

4 files changed

+83
-6
lines changed

jupyter_client/manager.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,19 +241,34 @@ def start_kernel(self, **kw):
241241
# If set, it can bork all the things.
242242
env.pop('PYTHONEXECUTABLE', None)
243243
if not self.kernel_cmd:
244-
# If kernel_cmd has been set manually, don't refer to a kernel spec
245-
# Environment variables from kernel spec are added to os.environ
246-
env.update(self.kernel_spec.env or {})
244+
# If kernel_cmd has been set manually, don't refer to a kernel spec.
245+
# Environment variables from kernel spec are added to os.environ.
246+
env.update(self._get_env_substitutions(self.kernel_spec.env, env))
247247
elif self.extra_env:
248-
env.update(self.extra_env)
248+
env.update(self._get_env_substitutions(self.extra_env, env))
249249

250250
# launch the kernel subprocess
251251
self.log.debug("Starting kernel: %s", kernel_cmd)
252-
self.kernel = self._launch_kernel(kernel_cmd, env=env,
253-
**kw)
252+
self.kernel = self._launch_kernel(kernel_cmd, env=env, **kw)
254253
self.start_restarter()
255254
self._connect_control_socket()
256255

256+
def _get_env_substitutions(self, templated_env, substitution_values):
257+
""" Walks env entries in templated_env and applies possible substitutions from current env
258+
(represented by substitution_values).
259+
Returns the substituted list of env entries.
260+
"""
261+
substituted_env = {}
262+
if templated_env:
263+
from string import Template
264+
265+
# For each templated env entry, fill any templated references
266+
# matching names of env variables with those values and build
267+
# new dict with substitutions.
268+
for k, v in templated_env.items():
269+
substituted_env.update({k: Template(v).safe_substitute(substitution_values)})
270+
return substituted_env
271+
257272
def request_shutdown(self, restart=False):
258273
"""Send a shutdown request via control channel
259274
"""

jupyter_client/tests/signalkernel.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Distributed under the terms of the Modified BSD License.
55

66
from __future__ import print_function
7+
import os
78

89
from subprocess import Popen, PIPE
910
import sys
@@ -38,6 +39,8 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None,
3839
reply['user_expressions']['pid'] = self.children[-1].pid
3940
elif code == 'check':
4041
reply['user_expressions']['poll'] = [ child.poll() for child in self.children ]
42+
elif code == 'env':
43+
reply['user_expressions']['env'] = os.getenv("TEST_VARS", "")
4144
elif code == 'sleep':
4245
try:
4346
time.sleep(10)

jupyter_client/tests/test_kernelmanager.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def _install_test_kernel(self):
4141
'-m', 'jupyter_client.tests.signalkernel',
4242
'-f', '{connection_file}'],
4343
'display_name': "Signal Test Kernel",
44+
'env': {'TEST_VARS': '${TEST_VARS}:test_var_2'},
4445
}))
4546

4647
def _get_tcp_km(self):
@@ -131,6 +132,63 @@ def test_start_new_kernel(self):
131132
self.assertTrue(km.is_alive())
132133
self.assertTrue(kc.is_alive())
133134

135+
def _env_test_body(self, kc):
136+
137+
def execute(cmd):
138+
kc.execute(cmd)
139+
reply = kc.get_shell_msg(TIMEOUT)
140+
content = reply['content']
141+
self.assertEqual(content['status'], 'ok')
142+
return content
143+
144+
reply = execute('env')
145+
self.assertIsNotNone(reply)
146+
self.assertEquals(reply['user_expressions']['env'], 'test_var_1:test_var_2')
147+
148+
def test_templated_kspec_env(self):
149+
self._install_test_kernel()
150+
km, kc = start_new_kernel(kernel_name='signaltest')
151+
self.addCleanup(kc.stop_channels)
152+
self.addCleanup(km.shutdown_kernel)
153+
154+
self.assertTrue(km.is_alive())
155+
self.assertTrue(kc.is_alive())
156+
157+
self._env_test_body(kc)
158+
159+
def _start_kernel_with_cmd(self, kernel_cmd, extra_env, **kwargs):
160+
"""Start a new kernel, and return its Manager and Client"""
161+
km = KernelManager(kernel_name='signaltest')
162+
km.kernel_cmd = kernel_cmd
163+
km.extra_env = extra_env
164+
km.start_kernel(**kwargs)
165+
kc = km.client()
166+
kc.start_channels()
167+
try:
168+
kc.wait_for_ready(timeout=60)
169+
except RuntimeError:
170+
kc.stop_channels()
171+
km.shutdown_kernel()
172+
raise
173+
174+
return km, kc
175+
176+
def test_templated_extra_env(self):
177+
self._install_test_kernel()
178+
kernel_cmd = [sys.executable,
179+
'-m', 'jupyter_client.tests.signalkernel',
180+
'-f', '{connection_file}']
181+
extra_env = {'TEST_VARS': '${TEST_VARS}:test_var_2'}
182+
183+
km, kc = self._start_kernel_with_cmd(kernel_cmd, extra_env)
184+
self.addCleanup(kc.stop_channels)
185+
self.addCleanup(km.shutdown_kernel)
186+
187+
self.assertTrue(km.is_alive())
188+
self.assertTrue(kc.is_alive())
189+
190+
self._env_test_body(kc)
191+
134192

135193
class TestParallel:
136194

jupyter_client/tests/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def start(self):
2929
'JUPYTER_DATA_DIR': pjoin(td.name, 'jupyter_data'),
3030
'JUPYTER_RUNTIME_DIR': pjoin(td.name, 'jupyter_runtime'),
3131
'IPYTHONDIR': pjoin(td.name, 'ipython'),
32+
'TEST_VARS': 'test_var_1',
3233
})
3334
self.env_patch.start()
3435

0 commit comments

Comments
 (0)