Kernel customisation (notebook)¶
uv managed virtual environment¶
We can use uv to create a virtual environment for different versions of Python.
Let's create a new virtual environment in the folder named ".venv_py315" using uv.
Requires a recent version of uv to be installed.
%uv venv .venv_py315 --python 3.15 --clear
%uv pip install --directory .venv_py315 --python 3.15 async-kernel
Downloading cpython-3.15.0b1-linux-x86_64-gnu (download) (35.5MiB)
Now we can write a kernel spec that uses uv to start the kernel from the virtual environment.
import pathlib
from async_kernel.kernelspec import write_kernel_spec
uv_path = pathlib.Path.cwd().joinpath(".venv_py315")
assert uv_path.exists()
write_kernel_spec(
name="async_3.15",
display_name="Python 3.15 (async)",
env={"UV_PROJECT_ENVIRONMENT": str(uv_path)},
command=("uv", "run", "--module", "async_kernel", "start"),
)
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) File ~/work/async-kernel/async-kernel/src/async_kernel/shell/ipshell.py:624, in IPShell.run_code(self, code_obj, result, async_) 622 await eval(code_obj, self.user_global_ns, self.user_ns) 623 else: --> 624 exec(code_obj, self.user_global_ns, self.user_ns) 625 except self.custom_exceptions: 626 etype, value, tb = sys.exc_info() Cell In[2], line 6 2 3 from async_kernel.kernelspec import write_kernel_spec 4 5 uv_path = pathlib.Path.cwd().joinpath(".venv_py315") ----> 6 assert uv_path.exists() 7 8 write_kernel_spec( 9 name="async_3.15", AssertionError:
The kernel spec with the display name "Python 3.15 (async kernel)" has been added. You will need to refresh the list of kernels for it to be available.
Embed the custom kernel in the kernelspec folder¶
When a callable is passed as the argument launcher to the function write_kernel_spec it is saved as a python file in the kernel spec folder. It will be used as the 'launcher' for the kernelspec.
Let's write a kernel that will echo (print) the code written to the cell.
import async_kernel.kernelspec
def launcher(settings: dict) -> None:
from async_kernel import Kernel
from async_kernel.interface.zmq import ZMQInterface
class EchoKernel(Kernel):
async def execute_request(self, job):
print(job["msg"]["content"]["code"])
return {"status": "ok", "execution_count": 0, "user_expressions": {}}
ZMQInterface.launch_instance(kernel_class=EchoKernel)
# Write the kernel spec
async_kernel.kernelspec.write_kernel_spec(name="echo", display_name="Echo kernel", launcher=launcher)
PosixPath('/home/runner/work/async-kernel/async-kernel/.venv/share/jupyter/kernels/echo')
Customize the shell¶
Sometimes it might be useful to customise the shell instead of the kernel.
Lets write IPshell as an echo a bypass keyword "# call".
import async_kernel.kernelspec
def launcher(settings: dict) -> None:
from traitlets import traitlets
from async_kernel.interface.zmq import ZMQInterface
from async_kernel.shell import IPShell
class EchoShell(IPShell):
@traitlets.default("banner1")
def _default_banner1(self) -> str:
return "Echo kernel with bypass\n"
def transform_cell(self, raw_cell):
if raw_cell.startswith("# call"):
return super().transform_cell(raw_cell)
return f'print("""{raw_cell}""")'
ZMQInterface.launch_instance(shell_class=EchoShell)
# Write the kernel spec
async_kernel.kernelspec.write_kernel_spec(
name="echo-shell-with-bypass", display_name="Echo with bypass", launcher=launcher
)
PosixPath('/home/runner/work/async-kernel/async-kernel/.venv/share/jupyter/kernels/echo-shell-with-bypass')
Configuration options¶
Configuration is done using traitlets configuration. Configuration options are available from the command line.
!async-kernel --help-all