Skip to content

Commit 8f7a865

Browse files
authored
Merge pull request #261 from takluyver/discovery
Prototype new kernel discovery machinery
2 parents f40dcd3 + 1f74c5f commit 8f7a865

File tree

6 files changed

+333
-16
lines changed

6 files changed

+333
-16
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ with Jupyter kernels.
2323

2424
kernels
2525
wrapperkernels
26+
kernel_providers
2627

2728
.. toctree::
2829
:maxdepth: 2

docs/kernel_providers.rst

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
================
2+
Kernel providers
3+
================
4+
5+
.. note::
6+
This is a new interface under development, and may still change.
7+
Not all Jupyter applications use this yet.
8+
See :ref:`kernelspecs` for the established way of discovering kernel types.
9+
10+
Creating a kernel provider
11+
==========================
12+
13+
By writing a kernel provider, you can extend how Jupyter applications discover
14+
and start kernels. For example, you could find kernels in an environment system
15+
like conda, or kernels on remote systems which you can access.
16+
17+
To write a kernel provider, subclass
18+
:class:`jupyter_client.discovery.KernelProviderBase`, giving your provider an ID
19+
and overriding two methods.
20+
21+
.. class:: MyKernelProvider
22+
23+
.. attribute:: id
24+
25+
A short string identifying this provider. Cannot contain forward slash
26+
(``/``).
27+
28+
.. method:: find_kernels()
29+
30+
Get the available kernel types this provider knows about.
31+
Return an iterable of 2-tuples: (name, attributes).
32+
*name* is a short string identifying the kernel type.
33+
*attributes* is a dictionary with information to allow selecting a kernel.
34+
35+
.. method:: make_manager(name)
36+
37+
Prepare and return a :class:`~jupyter_client.KernelManager` instance
38+
ready to start a new kernel instance of the type identified by *name*.
39+
The input will be one of the names given by :meth:`find_kernels`.
40+
41+
For example, imagine we want to tell Jupyter about kernels for a new language
42+
called *oblong*::
43+
44+
# oblong_provider.py
45+
from jupyter_client.discover import KernelProviderBase
46+
from jupyter_client import KernelManager
47+
from shutil import which
48+
49+
class OblongKernelProvider(KernelProviderBase):
50+
id = 'oblong'
51+
52+
def find_kernels(self):
53+
if not which('oblong-kernel'):
54+
return # Check it's available
55+
56+
# Two variants - for a real kernel, these could be something like
57+
# different conda environments.
58+
yield 'standard', {
59+
'display_name': 'Oblong (standard)',
60+
'language': {'name': 'oblong'},
61+
'argv': ['oblong-kernel'],
62+
}
63+
yield 'rounded', {
64+
'display_name': 'Oblong (rounded)',
65+
'language': {'name': 'oblong'},
66+
'argv': ['oblong-kernel'],
67+
}
68+
69+
def make_manager(self, name):
70+
if name == 'standard':
71+
return KernelManager(kernel_cmd=['oblong-kernel'],
72+
extra_env={'ROUNDED': '0'})
73+
elif name == 'rounded':
74+
return KernelManager(kernel_cmd=['oblong-kernel'],
75+
extra_env={'ROUNDED': '1'})
76+
else:
77+
raise ValueError("Unknown kernel %s" % name)
78+
79+
You would then register this with an *entry point*. In your ``setup.py``, put
80+
something like this::
81+
82+
setup(...
83+
entry_points = {
84+
'jupyter_client.kernel_providers' : [
85+
# The name before the '=' should match the id attribute
86+
'oblong = oblong_provider:OblongKernelProvider',
87+
]
88+
})
89+
90+
Finding kernel types
91+
====================
92+
93+
To find and start kernels in client code, use
94+
:class:`jupyter_client.discovery.KernelFinder`. This uses multiple kernel
95+
providers to find available kernels. Like a kernel provider, it has methods
96+
``find_kernels`` and ``make_manager``. The kernel names it works
97+
with have the provider ID as a prefix, e.g. ``oblong/rounded`` (from the example
98+
above).
99+
100+
::
101+
102+
from jupyter_client.discovery import KernelFinder
103+
kf = KernelFinder.from_entrypoints()
104+
105+
## Find available kernel types
106+
for name, attributes in kf.find_kernels():
107+
print(name, ':', attributes['display_name'])
108+
# oblong/standard : Oblong (standard)
109+
# oblong/rounded : Oblong(rounded)
110+
# ...
111+
112+
## Start a kernel by name
113+
manager = kf.make_manager('oblong/standard')
114+
manager.start_kernel()
115+
116+
.. module:: jupyter_client.discovery
117+
118+
.. autoclass:: KernelFinder
119+
120+
.. automethod:: from_entrypoints
121+
122+
.. automethod:: find_kernels
123+
124+
.. automethod:: make_manager
125+
126+
Kernel providers included in ``jupyter_client``
127+
===============================================
128+
129+
``jupyter_client`` includes two kernel providers:
130+
131+
.. autoclass:: KernelSpecProvider
132+
133+
.. seealso:: :ref:`kernelspecs`
134+
135+
.. autoclass:: IPykernelProvider
136+
137+
Glossary
138+
========
139+
140+
Kernel instance
141+
A running kernel, a process which can accept ZMQ connections from frontends.
142+
Its state includes a namespace and an execution counter.
143+
144+
Kernel type
145+
The software to run a kernel instance, along with the context in which a
146+
kernel starts. One kernel type allows starting multiple, initially similar
147+
kernel instances. For instance, one kernel type may be associated with one
148+
conda environment containing ``ipykernel``. The same kernel software in
149+
another environment would be a different kernel type. Another software package
150+
for a kernel, such as ``IRkernel``, would also be a different kernel type.
151+
152+
Kernel provider
153+
A Python class to discover kernel types and allow a client to start instances
154+
of those kernel types. For instance, one kernel provider might find conda
155+
environments containing ``ipykernel`` and allow starting kernel instances in
156+
these environments.

jupyter_client/discovery.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from abc import ABCMeta, abstractmethod
2+
import entrypoints
3+
import logging
4+
import six
5+
6+
from .kernelspec import KernelSpecManager
7+
from .manager import KernelManager
8+
9+
log = logging.getLogger(__name__)
10+
11+
class KernelProviderBase(six.with_metaclass(ABCMeta, object)):
12+
id = None # Should be a short string identifying the provider class.
13+
14+
@abstractmethod
15+
def find_kernels(self):
16+
"""Return an iterator of (kernel_name, kernel_info_dict) tuples."""
17+
pass
18+
19+
@abstractmethod
20+
def make_manager(self, name):
21+
"""Make and return a KernelManager instance to start a specified kernel
22+
23+
name will be one of the kernel names produced by find_kernels()
24+
"""
25+
pass
26+
27+
class KernelSpecProvider(KernelProviderBase):
28+
"""Offers kernel types from installed kernelspec directories.
29+
"""
30+
id = 'spec'
31+
32+
def __init__(self):
33+
self.ksm = KernelSpecManager()
34+
35+
def find_kernels(self):
36+
for name, resdir in self.ksm.find_kernel_specs().items():
37+
spec = self.ksm._get_kernel_spec_by_name(name, resdir)
38+
yield name, {
39+
# TODO: get full language info
40+
'language': {'name': spec.language},
41+
'display_name': spec.display_name,
42+
'argv': spec.argv,
43+
}
44+
45+
def make_manager(self, name):
46+
spec = self.ksm.get_kernel_spec(name)
47+
return KernelManager(kernel_cmd=spec.argv, extra_env=spec.env)
48+
49+
50+
class IPykernelProvider(KernelProviderBase):
51+
"""Offers a kernel type using the Python interpreter it's running in.
52+
53+
This checks if ipykernel is importable first.
54+
"""
55+
id = 'pyimport'
56+
57+
def _check_for_kernel(self):
58+
try:
59+
from ipykernel.kernelspec import RESOURCES, get_kernel_dict
60+
from ipykernel.ipkernel import IPythonKernel
61+
except ImportError:
62+
return None
63+
else:
64+
return {
65+
'spec': get_kernel_dict(),
66+
'language_info': IPythonKernel.language_info,
67+
'resources_dir': RESOURCES,
68+
}
69+
70+
def find_kernels(self):
71+
info = self._check_for_kernel()
72+
73+
if info:
74+
yield 'kernel', {
75+
'language': info['language_info'],
76+
'display_name': info['spec']['display_name'],
77+
'argv': info['spec']['argv'],
78+
}
79+
80+
def make_manager(self, name):
81+
info = self._check_for_kernel()
82+
if info is None:
83+
raise Exception("ipykernel is not importable")
84+
return KernelManager(kernel_cmd=info['spec']['argv'])
85+
86+
87+
class KernelFinder(object):
88+
"""Manages a collection of kernel providers to find available kernel types
89+
90+
*providers* should be a list of kernel provider instances.
91+
"""
92+
def __init__(self, providers):
93+
self.providers = providers
94+
95+
@classmethod
96+
def from_entrypoints(cls):
97+
"""Load all kernel providers advertised by entry points.
98+
99+
Kernel providers should use the "jupyter_client.kernel_providers"
100+
entry point group.
101+
102+
Returns an instance of KernelFinder.
103+
"""
104+
providers = []
105+
for ep in entrypoints.get_group_all('jupyter_client.kernel_providers'):
106+
try:
107+
provider = ep.load()() # Load and instantiate
108+
except Exception:
109+
log.error('Error loading kernel provider', exc_info=True)
110+
else:
111+
providers.append(provider)
112+
113+
return cls(providers)
114+
115+
def find_kernels(self):
116+
"""Iterate over available kernel types.
117+
118+
Yields 2-tuples of (prefixed_name, attributes)
119+
"""
120+
for provider in self.providers:
121+
for kid, attributes in provider.find_kernels():
122+
id = provider.id + '/' + kid
123+
yield id, attributes
124+
125+
def make_manager(self, name):
126+
"""Make a KernelManager instance for a given kernel type.
127+
"""
128+
provider_id, kernel_id = name.split('/', 1)
129+
for provider in self.providers:
130+
if provider_id == provider.id:
131+
return provider.make_manager(kernel_id)

jupyter_client/manager.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from ipython_genutils.importstring import import_item
2323
from .localinterfaces import is_local_ip, local_ips
2424
from traitlets import (
25-
Any, Float, Instance, Unicode, List, Bool, Type, DottedObjectName
25+
Any, Float, Instance, Unicode, List, Bool, Type, DottedObjectName, Dict
2626
)
2727
from jupyter_client import (
2828
launch_kernel,
@@ -87,23 +87,13 @@ def kernel_spec(self):
8787
self._kernel_spec = self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
8888
return self._kernel_spec
8989

90-
kernel_cmd = List(Unicode(), config=True,
91-
help="""DEPRECATED: Use kernel_name instead.
92-
93-
The Popen Command to launch the kernel.
94-
Override this if you have a custom kernel.
95-
If kernel_cmd is specified in a configuration file,
96-
Jupyter does not pass any arguments to the kernel,
97-
because it cannot make any assumptions about the
98-
arguments that the kernel understands. In particular,
99-
this means that the kernel does not receive the
100-
option --debug if it given on the Jupyter command line.
101-
"""
90+
kernel_cmd = List(Unicode(),
91+
help="""The Popen Command to launch the kernel."""
10292
)
10393

104-
def _kernel_cmd_changed(self, name, old, new):
105-
warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
106-
"start different kernels.")
94+
extra_env = Dict(
95+
help="""Extra environment variables to be set for the kernel."""
96+
)
10797

10898
@property
10999
def ipykernel(self):
@@ -254,6 +244,8 @@ def start_kernel(self, **kw):
254244
# If kernel_cmd has been set manually, don't refer to a kernel spec
255245
# Environment variables from kernel spec are added to os.environ
256246
env.update(self.kernel_spec.env or {})
247+
elif self.extra_env:
248+
env.update(self.extra_env)
257249

258250
# launch the kernel subprocess
259251
self.log.debug("Starting kernel: %s", kernel_cmd)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import sys
2+
3+
from jupyter_client import KernelManager
4+
from jupyter_client import discovery
5+
6+
def test_ipykernel_provider():
7+
import ipykernel # Fail clearly if ipykernel not installed
8+
ikf = discovery.IPykernelProvider()
9+
10+
res = list(ikf.find_kernels())
11+
assert len(res) == 1, res
12+
id, info = res[0]
13+
assert id == 'kernel'
14+
assert info['argv'][0] == sys.executable
15+
16+
class DummyKernelProvider(discovery.KernelProviderBase):
17+
"""A dummy kernel provider for testing KernelFinder"""
18+
id = 'dummy'
19+
20+
def find_kernels(self):
21+
yield 'sample', {'argv': ['dummy_kernel']}
22+
23+
def make_manager(self, name):
24+
return KernelManager(kernel_cmd=['dummy_kernel'])
25+
26+
def test_meta_kernel_finder():
27+
kf = discovery.KernelFinder(providers=[DummyKernelProvider()])
28+
assert list(kf.find_kernels()) == \
29+
[('dummy/sample', {'argv': ['dummy_kernel']})]
30+
31+
manager = kf.make_manager('dummy/sample')
32+
assert manager.kernel_cmd == ['dummy_kernel']

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def run(self):
8282
'jupyter_core',
8383
'pyzmq>=13',
8484
'python-dateutil>=2.1',
85+
'entrypoints',
8586
],
8687
extras_require = {
8788
'test': ['ipykernel', 'ipython', 'mock', 'pytest'],
@@ -93,6 +94,10 @@ def run(self):
9394
'console_scripts': [
9495
'jupyter-kernelspec = jupyter_client.kernelspecapp:KernelSpecApp.launch_instance',
9596
'jupyter-run = jupyter_client.runapp:RunApp.launch_instance',
97+
],
98+
'jupyter_client.kernel_providers' : [
99+
'spec = jupyter_client.discovery:KernelSpecProvider',
100+
'pyimport = jupyter_client.discovery:IPykernelProvider',
96101
]
97102
},
98103
)

0 commit comments

Comments
 (0)