Skip to content

Commit dc213de

Browse files
authored
Merge pull request #472 from kevin-bates/templated-env
Add support for templated env entries
2 parents 2a43817 + 5d37a3a commit dc213de

File tree

5 files changed

+87
-7
lines changed

5 files changed

+87
-7
lines changed

docs/kernels.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ JSON serialised dictionary containing the following keys and values:
152152
the client will default to ``signal`` mode.
153153
- **env** (optional): A dictionary of environment variables to set for the kernel.
154154
These will be added to the current environment variables before the kernel is
155-
started.
155+
started. Existing environment variables can be referenced using ``${<ENV_VAR>}`` and
156+
will be substituted with the corresponding value. Administrators should note that use
157+
of ``${<ENV_VAR>}`` can expose sensitive variables and should use only in controlled
158+
circumstances.
156159
- **metadata** (optional): A dictionary of additional attributes about this
157160
kernel; used by clients to aid in kernel selection. Metadata added
158161
here should be namespaced for the tool reading and writing that metadata.

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)