Skip to content

Commit

Permalink
Add iqsh kernel
Browse files Browse the repository at this point in the history
Add Jupyter kernel that connects to qsh. Implement basic execution and
code completion for the kernel.

To install the kernel, run:

    python -m libqtile.interactive.iqsh_install

To run the kernel in a standalone instance, you can run:

    python -m libqtile.interactive.iqsh_kernel

To it will likely be easier to run spin up a kernel and run straight
from a Jupyter console:

    jupyter console --kernel iqsh

Or run the helper scripts (which just executes the above), `iqsh`.
  • Loading branch information
flacjacket committed Feb 29, 2016
1 parent e5cec39 commit 7a6e035
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 11 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ prune scripts
prune test
prune debian
prune rpm
include bin/iqsh

recursive-exclude * __pycache__
recursive-exclude * *.py[co]
23 changes: 23 additions & 0 deletions bin/iqsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

# Copyright (c) 2008, Aldo Cortesi. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

jupyter console --kernel qsh
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Commands and scripting
manual/commands/index
manual/commands/scripting
manual/commands/qsh
manual/commands/iqsh

Getting involved
================
Expand Down
2 changes: 2 additions & 0 deletions docs/manual/commands/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ different places:
configuration file.
* Commands can be :doc:`called through qsh </manual/commands/qsh>`, the Qtile
shell.
* The qsh can also be hooked into a Jupyter kernel :doc:`called iqsh
</manual/commands/iqsh>`.
* Commands can be :doc:`called from a script </manual/commands/scripting>` to
interact with Qtile from Python.

Expand Down
65 changes: 65 additions & 0 deletions docs/manual/commands/iqsh.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
====
iqsh
====

In addition to the standard ``qsh`` shell interface, we provide a kernel
capable of running through Jupyter that hooks into the qsh client. The command
structure and syntax is the same as qsh, so it is recommended you read that for
more information about that.

Dependencies
============

In order to run iqsh, you must have ``ipykernel`` and ``jupyter_console``. You
can install the dependencies when you are installing qtile by running:

.. code-block:: bash
$ pip install qtile[ipython]
Otherwise, you can just install these two packages separately.

Installing and Running the Kernel
=================================

Once you have the required dependencies, you can run the kernel right away by running:

.. code-block:: bash
$ python -m libqtile.interactive.iqsh_kernel
However, this will merely spawn a kernel instance, you will have to run a
separate frontend that connects to this kernel.

A more convenient way to run the kernel is by registering the kernel with
Jupyter. To register the kernel itself, run:

.. code-block:: bash
$ python -m libqtile.interactive.iqsh_install
If you run this as a non-root user, or pass the ``--user`` flag, this will
install to the user Jupyter kernel directory. You can now invoke the kernel
directly when starting a Jupyter frontend, for example:

.. code-block:: bash
$ jupyter console --kernel qsh
The ``iqsh`` script will launch a Jupyter terminal console with the qsh kernel.

iqsh vs qsh
===========

One of the main drawbacks of running through a Jupyter kernel is the frontend
has no way to query the current node of the kernel, and as such, there is no
way to set a custom prompt. In order to query your current node, you can call
``pwd``.

This, however, enables many of the benefits of running in a Jupyter frontend,
including being able to save, run, and re-run code cells in frontends such as
the Jupyter notebook.

The Jupyter kernel also enables more advanced help, text completion, and
introspection capabilities (however, these are currently not implemented at a
level much beyond what is available in the standard qsh).
54 changes: 54 additions & 0 deletions libqtile/interactive/iqsh_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) 2016 Sean Vig
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import json
import os
import sys

from jupyter_client.kernelspec import install_kernel_spec

# On Python 3, TemporaryDirectory works with context manager
try:
from tempfile import TemporaryDirectory
except ImportError:
from IPython.utils.tempdir import TemporaryDirectory

kernel_json = {
"argv": [sys.executable, "-m", "libqtile.interactive.iqsh_kernel", "-f", "{connection_file}"],
"display_name": "iqsh",
"language": "qsh",
}


def main(argv=[]):
user = '--user' in argv or os.geteuid() != 0

with TemporaryDirectory() as td:
# IPython tempdir starts off as 700, not user readable
os.chmod(td, 0o755)
with open(os.path.join(td, 'kernel.json'), 'w') as f:
json.dump(kernel_json, f, sort_keys=True)

print('Installing IPython kernel spec')
install_kernel_spec(td, 'qsh', user=user, replace=True)


if __name__ == '__main__':
main(sys.argv)
106 changes: 106 additions & 0 deletions libqtile/interactive/iqsh_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (c) 2016, Sean Vig
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from libqtile import sh, command

from ipykernel.kernelbase import Kernel


class QshKernel(Kernel):
implementation = 'qsh'
implementation_version = '0.1'
language = 'no-op'
language_version = '1.0'
language_info = {'mimetype': 'text/plain'}
banner = "Qsh Kernel"

def __init__(self, **kwargs):
Kernel.__init__(self, **kwargs)
self.client = command.Client()
self.qsh = sh.QSh(self.client)

def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False):
# if no command sent, just return
if not code.strip():
return {
'status': 'ok',
'execution_count': self.execution_count,
'payload': [],
'user_expressions': {},
}

if code[-1] == '?':
return self.do_inspect(code, len(code) - 1)

try:
output = self.qsh.process_command(code)
except KeyboardInterrupt:
return {
'status': 'abort',
'execution_count': self.execution_count,
}

if not silent and output:
stream_content = {'name': 'stdout', 'text': output}
self.send_response(self.iopub_socket, 'stream', stream_content)

return {
'status': 'ok',
'execution_count': self.execution_count,
'payload': [],
'user_expressions': {},
}

def do_complete(self, code, cursor_pos):
no_complete = {
'status': 'ok',
'matches': [],
'cursor_start': 0,
'cursor_end': cursor_pos,
'metadata': dict(),
}

if not code or code[-1] == ' ':
return no_complete

tokens = code.split()
if not tokens:
return no_complete

token = tokens[-1]
start = cursor_pos - len(token)

matches = self.qsh._complete(code, token)
return {
'status': 'ok',
'matches': sorted(matches),
'cursor_start': start,
'cursor_end': cursor_pos,
'metadata': dict(),
}


def main():
from ipykernel.kernelapp import IPKernelApp
IPKernelApp.launch_instance(kernel_class=QshKernel)


if __name__ == '__main__':
main()
7 changes: 7 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ def finalize_options(self):
license="MIT",
install_requires=dependencies,
setup_requires=dependencies,
extras_require={
'ipython': ["ipykernel", "jupyter_console"],
},
packages=['libqtile',
'libqtile.interactive',
'libqtile.layout',
'libqtile.scripts',
'libqtile.widget',
Expand All @@ -148,6 +152,9 @@ def finalize_options(self):
'qsh = libqtile.scripts.qsh:main',
]
},
scripts=[
'bin/iqsh',
],
data_files=[
('share/man/man1', ['resources/qtile.1',
'resources/qsh.1'])],
Expand Down
26 changes: 15 additions & 11 deletions test/test_sh.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ def test_findNode(self):
@Xephyr(True, ShConfig())
def test_do_cd(self):
self.sh = libqtile.sh.QSh(self.c)
assert not self.sh.do_cd("layout")
assert self.sh.do_cd("0/wibble")
assert not self.sh.do_cd("0/")
assert self.sh.do_cd("layout") == 'layout'
assert self.sh.do_cd("0/wibble") == 'No such path.'
assert self.sh.do_cd("0/") == 'layout[0]'


@Xephyr(True, ShConfig())
Expand All @@ -115,14 +115,18 @@ def test_call(self):
@Xephyr(True, ShConfig())
def test_complete(self):
self.sh = libqtile.sh.QSh(self.c)
assert self.sh._complete("c", "c", 0) == "cd"
assert self.sh._complete("c", "c", 1) == "commands"
assert self.sh._complete("c", "c", 2) == "critical"
assert self.sh._complete("c", "c", 3) is None

assert self.sh._complete("cd l", "l", 0) == "layout"
assert self.sh._complete("cd layout/", "layout/", 0) == "layout/group"
assert self.sh._complete("cd layout/", "layout/g", 0) == "layout/group"
assert self.sh._complete("c", "c") == [
"cd",
"commands",
"critical",
]

assert self.sh._complete("cd l", "l") == ["layout"]
print(self.sh._complete("cd layout/", "layout/"))
assert self.sh._complete("cd layout/", "layout/") == [
"layout/" + x for x in ["group", "window", "screen", "0"]
]
assert self.sh._complete("cd layout/", "layout/g") == ["layout/group"]


@Xephyr(True, ShConfig())
Expand Down

0 comments on commit 7a6e035

Please sign in to comment.