How to Write a simple xonsh xontrib
Introduction
The xonsh
docs have a guide on how to write
xontribs, but I think this guide is a
bit brusk. It says everything it needs to say but leaves out a lot of the
detail that someone like me might need to create a xontrib. So, while I write a
xontrib for something else, I've decided to write a more detailed xontrib
composition guide.
Though this will show you how to create a xontrib, it is presented as a log of me making my way through this process rather than a traditional step-by-step how-to.
When I say "create a xontrib," this phrase deceptively implies there is one step in creating a xontrib everyone can use. If you want to share your xontrib with the world, the involved steps are to:
- Literally create xontrib code and load it into your xonsh shell.
- Make this xontrib a distribution installable with something like
pip
. - Host this xontrib distribution on PyPI.
- Register the xontrib with the xonsh project.
Once these steps are complete, the xontrib is ready for consumption by the good folks on the internet. If your aim is to write a xontrib solely for your personal use and never share it, you could stop at step 1 (but I'd recommend going to through step 2 as well).
For this guide, I'll focus only on steps 1 and 2, which should be enough to get anyone far down the path of developing and using a xontrib they have developed. Steps 3 is a more general Python distrubtion concern and is outlined in the Python Packaging User Guide section on the topic.
Part 4 is covered in the above linked doc on registering xontribs with the xonsh project.
Software versions
I'm using xonsh 0.9.3
and Python 3.7.3
for this guide.
Step 1: What makes a xontrib?
The xonsh
docs have a guide on how to write xontribs!
These docs briefly tell us how to make a xontrib:
Writing a xontrib is as easy as writing a xonsh or Python file and sticking it in a directory named
xontrib/
.
That seems so simple! But how could that work? The docs are implying that
creating any .xsh
or .py
file in a directory with the special name
xontrib/
will allow the xontrib to be imported and loaded by xontrib load
.
How could my xontrib/
directory somewhere on my filesystem get picked up and
added to a globally importable xontrib
package? The answer is implicit
namespaces. Implicit namespaces are a convention, introduced in PEP
420, that the import machinery uses
to construct a single package from multiple locations (these locations are
called "partials").
The specifics of implicit namespaces are a bit more complicated than this, but
for xontribs, this means almost exactly what we were told. We need to create
a .xsh
or .py
file inside a directory named xontrib/
; however, missing
from the docs is the important point that this xontrib/
directory cannot just
be anywhere on the file system - it must be available in the Python path.
This should all become clear after an example.
So, if we follow the implicit namespace conventions for our xontrib, it will be
available on the xontrib
package as a module. xontrib load
, the line you
are probably familiar with if you've used a
xontrib, takes the
name of a module in the xontrib
package, so xontrib load myxontribname
will load
the xontrib.myxontribname
module as a xontrib.
xontrib "Hello World!"
Here's a trivial xontrib "Hello World!" example using these concepts.
My first xontrib, called hello_world
has the following directory structure:
``` @eddie-ubuntu ~/source/xontrib-hello-world $ tree . └── xontrib └── hello_world.py
1 directory, 1 file ```
I created this by running the following in xonsh inside the xontrib-hello-world
directory:
eddie@eddie-ubuntu ~/source/xontrib-hello-world $ mkdir xontrib
eddie@eddie-ubuntu ~/source/xontrib-hello-world $ echo "print('Hello World!')" > xontrib/hello_world.py
Now, I want to load this xontrib, right? Let's see if xonsh has picked it up yet:
eddie@eddie-ubuntu ~/source/xontrib-hello-world $ xontrib load hello_world
The following xontribs are enabled but not installed:
hello_world
To install them run
xpip install
Hmm, this is a strange message, but it all makes sense when considering (1)
what a xontrib is, (2) how implicit namespaces work with xontribs, and (3) how
xontribs are normally installed. To break this message down, let's first tackle
the statement that the xontrib is enabled but not installed. Indeed, we just
enabled this xontrib with xontrib load hello_world
, but by saying the xontrib
is not installed, xonsh is effectively saying that the xontrib code cannot be
found. The second part of the message, which tells us to install the xontrib
via xpip install
would be helpful if we were trying to install a xontrib from
a distribution (a package with a setup.py
that is hosted on
PyPI, for instance). In our case, it isn't really helpful
since we are trying to load a xontrib locally using the bare minimum components
for creating a xontrib, so we can ignore this second message.
So, how do we install our trivial xontrib? Since we aren't installing a
distribution package and we're just relying on the import machinery's
understanding of implicit namespace packages, we have to make sure all the
requirements of that convention are satisfied. So far, we have done everything
to create a xontrib except making it discoverable via import machinery under
the xontrib
package. This next step will do that.
As usual, the way to make a package discoverable by import machinery is to add
its containing directory to the sys.path
global, so let's do that, picking up
immediately where we left off before with the "The following xontribs are
enabled but installed..." message:
``` eddie@eddie-ubuntu ~/source/xontrib-hello-world $ xontrib load hello_world The following xontribs are enabled but not installed: hello_world To install them run xpip install eddie@eddie-ubuntu ~/source/xontrib-hello-world $ from xontrib.voxapi import Vox eddie@eddie-ubuntu ~/source/xontrib-hello-world $ from sys import path eddie@eddie-ubuntu ~/source/xontrib-hello-world $ path ['/home/eddie/.virtualenvs/xonsh/bin', '/home/eddie/.pyenv/versions/3.7.3/lib/python37.zip', '/home/eddie/.pyenv/versions/3.7.3/lib/python3.7', '/home/eddie/.pyenv/versions/3.7.3/lib/python3.7/lib-dynload', '/home/eddie/.virtualenvs/xonsh/lib/python3.7/site-packages', '/home/eddie/source/xonsh', '/home/eddie/source/xontrib-z'] eddie@eddie-ubuntu ~/source/xontrib-hello-world $ tree . └── xontrib └── hello_world.py
1 directory, 1 file eddie@eddie-ubuntu ~/source/xontrib-hello-world $ path.append(os.getcwd()) eddie@eddie-ubuntu ~/source/xontrib-hello-world $ path ['/home/eddie/.virtualenvs/xonsh/bin', '/home/eddie/.pyenv/versions/3.7.3/lib/python37.zip', '/home/eddie/.pyenv/versions/3.7.3/lib/python3.7', '/home/eddie/.pyenv/versions/3.7.3/lib/python3.7/lib-dynload', '/home/eddie/.virtualenvs/xonsh/lib/python3.7/site-packages', '/home/eddie/source/xonsh', '/home/eddie/source/xontrib-z', '/home/eddie/source/xontrib-hello-world'] eddie@eddie-ubuntu ~/source/xontrib-hello-world $ xontrib load hello_world Hello World! ```
After adding the current directory, which contains the aforementioned
xontrib/
named directory, we are able to enabled the xontrib, and upon
loading it, we see that xonsh must think it's installed because it runs the
code inside the hello_world.py
file and prints "Hello World!" to the screen.
That concludes the creation of a super simple xontrib. The rest of this post
will be concerned with building up a distribution out of a simple xontrib. To
spoil the fun a bit, packaging up a xontrib is just a matter of getting
setuptools
to put our xontrib code in a xontrib/
directory that is already
in the sys.path
so that it can be loaded as a module on the xontrib
package.
Step 2: How do we make a xontrib installable with something like pip?
Python packages are described for easy installation through the setup.py
file.
I had not created a Python package before this one, though I was acquainted
with setup.py
files through the many examples I saw in GitHub projects, but I
never understood setup.py
very well.
In the fictional internet time between the last step and this one, I started
working on a real xontrib that I will use for the rest of this guide. This
means I'm abandoning the "Hello World!" xontrib and creating one called
xontrib-per-directory-history
. This will be like per-directory-history for
zsh, which I used for a
long time in my zsh
configuration and now miss dearly. The following steps
should be applicable regardless of the goal of the xontrib you are creating.
This is the setup.py
configuration I ended up with for this xontrib:
``` xontrib-per-directory-history
Per-directory history for xonsh, like zsh's https://github.com/jimhester/per-directory-history
Restricts history to those that were executed in the current directory, with keybindings to switch between that and global history. """
from setuptools import setup
setup( name='xontrib-per-directory-history', version='0.1', description="Per-directory history for xonsh, like zsh's https://github.com/jimhester/per-directory-history", long_description=doc, license='MIT', url='https://github.com/eppeters/xontrib-per-directory-history', author='Eddie Peters', author_email='edward.paul.peters@gmail.com', packages=['xontrib'], package_dir={'xontrib': 'xontrib'}, package_data={'xontrib': ['*.xsh']}, platforms='any', install_requires=[ 'xonsh>=0.9.3', ], classifiers=[ 'Environment :: Console', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: System :: Shells', 'Topic :: System :: System Shells', ] ) ```
xontrib packages have a particular layout that requires us to write some
boilerplate in the setup.py
. The xonsh docs link to a cookiecutter
template that you can use to
generate most of this, as well as the file structure, for you if you want.
Most of the kwargs
passed to setup
define obvious informational metadata
and won't be discussed here. They are useful for attribution, searching on
PyPI, etc., but won't affect the installability of your xontrib package. The
most interesting (read: least obvious) of these informational pieces is the
classifiers
list, which you can find out more about on PyPI's classifiers
page.
The following triplet effectively tells setuptools
to do everything we did manually in Step 1 above:
packages=['xontrib'],
package_dir={'xontrib': 'xontrib'},
package_data={'xontrib': ['*.xsh']},
packages
says that this package will provide a package named xontrib
,
package_dir
maps this xontrib
package to the directory that contains the
xontrib
package's code, and this package_data
setting tells setuptools
to
not only install .py
files into the package's final installation location,
but to also include any files matching the glob pattern *.xsh
. More details
on this setting can be found in the setuptools data files
documentation.
Conveniently, setuptools will install multiple distributions that claim to
create the same packages via the packages
directive without clobbering one
another (assuming their contents don't conflict). What this means for a xontrib
is that all the xontribs that use packages=['xontrib']
will have their .py
and .xsh
files installed into the site-packages
xontrib
directory
alongside the other installed xontribs' .py
and .xsh
files.
Have a look at the
xontrib/
directory inside my xonsh
virtualenv, for instance, which includes
several other xontribs' code:
eddie@eddie-ubuntu ~/.virtualenvs/xonsh $ ls lib/python3.7/site-packages/xontrib
fzf-widgets.xsh per-directory-history.xsh thefuck.py
Each of these .xsh
and .py
files were created by installing different
xontrib Python packages using pip
, yet their code lives in the same spot,
discoverable by import machinery using the implicit namespace packages
convention.
platforms
allows us to specify that a xontrib should be available on one
platform but not another. For mine, I chose 'any'
because I didn't expect to
introduce functionality that only works on some of xonsh's supported platforms.
So, now my xontrib is installable via pip
!
```
eddie@eddie-ubuntu ~/source/xontrib-per-directory-history master $ tree
.
├── setup.py
└── xontrib
└── per-directory-history.xsh
1 directory, 2 files
eddie@eddie-ubuntu ~/source/xontrib-per-directory-history master $ xpip install .
Processing /home/eddie/source/xontrib-per-directory-history
Requirement already satisfied: xonsh>=0.9.3 in /home/eddie/source/xonsh (from xontrib-per-directory-history==0.1) (0.9.3)
xontInstalling collected packages: xontrib-per-directory-history
rib Running setup.py install for xontrib-per-directory-history ... -ldone
Successfully installed xontrib-per-directory-history-0.1
aYou are using pip version 19.0.3, however version 19.1.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
eddie@eddie-ubuntu ~/source/xontrib-per-directory-history master $ xontrib load per-directory-history
hello world
```
And, yes, the only code in my xontrib's .py
file right now is print('hello world')
,
but that's beside the point.
Conclusion
I hope you've found this guide useful for getting started in developing a
xontrib, but obviously I've left out everything regarding what you can do with
a xontrib. xontribs allow hooking into shell functionality, such as that
provided by prompt
toolkit. Using
prompt toolkit
's keybinding functionality would allow you to install a new
keyboard command to run some code, for instance. For more examples and
inspiration for what can be done in a xontrib, I suggest checking out the
xontribs bundled with xonsh by
default. Happy hacking!
For information on sharing your xontrib with the world, check out the links in the introduction.