Click – Python library for command-line interfaces
click.pocoo.org
click.pocoo.org
https://github.com/seaneagan/unscripted
Checkout the github issues there for some features I want to add, like bash completion support for example. I think we can steal ideas from each other!
One nice thing about dart's annotations vs. python's decorators is they can be placed on a function's parameters as well, so it allows the option/flag/argument declarations to be a bit more DRY.
The main idea behind it is make it easy to write a "getopt" style program with arbitrary command nesting. I have found that although argparse and sisters are nice libraries they don't allow me to do many of things I like to do in my interfaces. For instance sometimes like options such as
foo -x a -x y -x q ...
where I would process that into like so: extras = list()
for opt, arg in opts:
if opt in ('-h', '--help',):
util.usage()
...
elif opt in ('-x', '--extra'):
extras.append(validate_or_die(arg))
I also believe that you should have "fast fail" validators. So I have several in `optutils` which are like: util.assert_dir_exists(path)
which if a directory doesn't exist on the path it creates it. If there is already a file there and it isn't a directory it dies with an error. When it dies, I try and have unique exit codes for various errors (for testability) and provide usage information immediately. This style is nice because it provides immediate feedback to the user with no fuss. I think a lot of "option parser frameworks" miss the point in having lots of things for parsing ints and things. Most of the time I deal with files, directories, and "string" parameters which these libraries don't help with.In general, the standard libraries make it way to hard to write really nice command line tools. I like some things about your library, but I think that you need to increase the flexibitly for how options are processsed to you can do whatever you want with them. I also think that option parsing and configuration should be integrated. I am working to support that but I am not there yet. (see optutils/conf.py for my current ideas)
?
This is trivial to do with argparse (or optparse for that matter):
import argparse
def a_prefixed(string):
if not string.startswith('a'):
raise argparse.ArgumentTypeError("%r does not start with 'a'" % string)
return string
parser = argparse.ArgumentParser()
parser.add_argument('-x', '--extra', action='append', type=a_prefixed)
print parser.parse_args()
resulting in: > python test.py -x afoo -x abar
Namespace(extra=['afoo', 'abar'])
> python test.py -x afoo -x baz
usage: test.py [-h] [-x EXTRA]
test.py: error: argument -x/--extra: 'baz' does not start with 'a'
> I also believe that you should have "fast fail" validators.Isn't that what the callback parameter is for, especially with is_eager=True? Or ParamType if you need either something more reusable or something more extensive.
> Most of the time I deal with files, directories, and "string" parameters which these libraries don't help with.
http://click.pocoo.org/api/#click.File, argparse has something similar.
> Isn't that what the callback parameter is for, especially with is_eager=True? Or ParamType if you need either something more reusable or something more extensive.
yes. I think almost everything should work like this. I think the getopt style makes this a bit easier to understand.
> http://click.pocoo.org/api/#click.File, argparse has something similar.
Not at all the same. I never said my programs were going to open the files themselves. I often have to write automation scripts around other things. In these cases I need to make sure files and directories are sane but I don't open them. I just canonicalize them and pass them on.
The big thing is the lack of integration with configuration files which is something I am still working on myself.
Its a plane, its a bird, No its kick ass python programmer. :).
I still prefer docopt (https://github.com/docopt/docopt) . It is so much simpler to use. Click seems to be a bit overengineered.
I could use it for simple cases within 15 seconds of reading. Docopt not so much.
The general idea is that you don't specify any code, you just give the help message as text. Docopt parses that to figure out all the options etc and convert those into the parameters coming in to your system.
It's a really clever idea. You specify the human interface, docopt converts that into the code version (so long as you adhere to a few common conventions). I haven't seen a cleaner system anywhere.
From their example:
"""Naval Fate.
Usage:
naval_fate.py ship new <name>...
naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
naval_fate.py ship shoot <x> <y>
naval_fate.py mine (set|remove) <x> <y> [--moored | --drifting]
naval_fate.py (-h | --help)
naval_fate.py --version
Options:
-h --help Show this screen.
--version Show version.
--speed=<kn> Speed in knots [default: 10].
--moored Moored (anchored) mine.
--drifting Drifting mine.
"""
from docopt import docopt
if __name__ == '__main__':
arguments = docopt(__doc__, version='Naval Fate 2.0')
print(arguments)When using docopt i spent much more time defining the docstring and figuring out the right way to write it, especially more complex scenarios.
docopt is much simpler because it's only capable of creating very trivial interfaces. That's perfectly fine if that's all what you want to do but it also makes docopt unusable, if that's not the case.
Usage:
script.py --option=<name>...
script.py --option spam --option eggs
{
"--option": [
"spam",
"eggs"
]
}
Example here [0].I haven't pushed it too far so I'm sure there are other cases where you'd need to create a different command line api to have it work (that may or may not be an issue depending on your use case).
Regarding the other criticism, seems valid. In the docopt api can you have it do the parsing for you so you can edit the rules afterwards before you run them?
[0] http://try.docopt.org/?doc=Usage%3A%0D%0A++script.py+--optio...
Take a look at the example in the README: https://github.com/docopt/docopt
I'm wondering if there could be a breakout at the next PyCon to see if we could discuss approaches and come up with a unified way to do convert Python libraries into command line scripts?
- Cliff http://cliff.readthedocs.org/en/latest/
- docopt http://docopt.org
- argparse
- optparse
etc etc etc...
"There are many alternatives to click and you can have a look at them if you enjoy them better. The obvious ones are optparse and argparse from the standard library.
click is actually implemented as a wrapper around optparse and does not implement any parsing itself. The reason it’s not based on argparse is that argparse‘s design does not allow proper nesting of commands by design and has some deficiencies when it comes to POSIX compliant argument handling."
What I love about Pocoo is they always have stellar documentation and give clear rationale - from the beginning.
On a side note, however, does anyone know why the team prefers to wrap functions in decorators? Flask also uses them, but what's the design decision behind them?
Where in js flask might read app.route('/', function(){//do something}); python uses a decorator right above a regular function definition. The alternative is to manually add it after it's defined (in flask that's add_url_rule), but that kind of hides the intent of the function and is more cumbersome as you need to manually pass in the fn name.
downside: both need external binarys.
[0]: http://pythondialog.sourceforge.net/ [1]: https://github.com/marwano/whiptail
>pip install click
Well... no you cannot. See the page: https://pypi.python.org/pypi/click
The package hasn't been uploaded yet. However one can install it straight from the git repo:
> pip install git+ssh://git@github.com:mitsuhiko/click.git
Can someone comment regarding using pip like that? Is it fine to put this on req.txt? Any best practices somewhere?
If you plan to release your stuff, the dependencies in your req.txt should be as pinned as possible. The classic example is Requests: when it changed the API fairly significantly, umpteen installers broke... just because people did not bother with specifying a version for that lib.
pip install "https://github.com/mitsuhiko/click/tarball/master#egg=click"
to pin a specific revision:
pip install "https://github.com/mitsuhiko/click/tarball/5b7b7296fabc5d47d...
(btw, it seems bad practice to add such a link as a dependency in your setup.py... usually you'd do it for temporary shallow forks, but otherwise I think it'd be better to also upload your shallow fork on pypi ...I guess you can just remove it from pypi if it won't be needed anymore)
Flask: Georgia for text, Garamond for titles
Werkzeug: Lucida Grande for text, Ubuntu for titles
And now click: Ubuntu Mono for text, Open Sans for titles
font-family: 'Ubuntu Mono','Consolas','Menlo','Deja Vu Sans Mono','Bitstream Vera Sans Mono';
...(fulfilled by Menlo on my Mac) for a future project.
I don't blame this particular offering, since I don't think release was actually planned just yet and any number of other projects have made the same subtle mistake.
Command Name Extensions are Harmful. Don't expose such an implementation detail in every example, lest everyone actually follow them. Use the "#!/usr/bin/env python" or whatever at the top of your scripts. And yes, you can keep the .py if what you have is a library, not just a command (but it's nice to then make a wrapper the doesn't expose the implementation language). And obviously in other OSes where the command extension can be omitted and still work this isn't such a big deal.
But in Unix/Linux, commands should be reimplementable in a different language without making some .(extension) a like, retained to keep from breaking other things that depend on it. Just say no :-)
It actually shows you how to set it up so you don't even use a #!, but rely on setuptools to make an executable for you, that'll work in a vitualenv or on windows. And the script name doesn't have .py at the end in his example, though he doesn't call that out specifically.
- Cement (http://builtoncement.com)
- Cliff (http://cliff.readthedocs.org/en/latest/)
- Plumbum (http://plumbum.readthedocs.org)
- Argh (https://pypi.python.org/pypi/argh/0.24.1)
- Aaargh (https://github.com/wbolster/aaargh)
- Baker (https://pypi.python.org/pypi/Baker/)
So many more to choose from. Now we get to evaluate Click. Seems like the reason Armin wrote Click was to load options dynamically, but that's what Cliff does via stevedore (https://github.com/dreamhost/stevedore).
My favorite feature about Cliff though is: http://cliff.readthedocs.org/en/latest/complete.html which comes out of the box, but then again, there's Argcomplete (https://github.com/kislyuk/argcomplete).
EDIT: Updating from previous posters
- Naked (http://naked-py.com)
- Docopt (http://docopt.org)
- Clint (https://github.com/kennethreitz/clint)
- Argvard (https://github.com/DasIch/argvard)
- Commandr (https://github.com/tellapart/commandr)
- Argtools (https://pypi.python.org/pypi/argtools/0.1.2)
- Plac (https://pypi.python.org/pypi/plac)
Their example:
@click.command()
@click.option('--count', default=1, help='number of greetings')
@click.option('--name', prompt='Your name',
help='the person to greet', required=True)
def hello(count, name):
for x in range(count):
print('Hello %s!' % name)
Could be written as: def hello(count, name):
for x in range(count):
print('Hello %s!' % name
hello = click.option('--name', prompt='Your name',
help='the person to greet', required=True)(hello)
hello = click.option('--count', default=1, help='number of greetings')(hello)
hello = click.command(hello)In short running `foo --help` should be instant and if it loads all its modules to list the different sub-commands and their respective description it is really too slow.
A possibility is to cache some information (e.g. generate a text file or a small Python script).
What startup time?
I even have a python script running on every shell prompt drawing (that checks mercurial on top of starting the python interpreter), and the latency is negligible.
> time python manage.py --help
real 0m0.473s
user 0m0.240s
sys 0m0.148s
Half of a second is very noticeable (and annoying). I had similar perception in my previous work.You can try to only load enough to display the help text without really loading everything, but that doesn't work that well (you have to organize things differently, and `--help` requires to load a lot of stuff. `subcommand --help` needs less but it still has to see if the subcommand exists).
As a reference:
> time python -c 'print "hello"'
hello
real 0m0.041s
user 0m0.024s
sys 0m0.012s
> time echo hello
hello
real 0m0.000s
user 0m0.000s
sys 0m0.000s
Actually even the first (0.041s) is not instant, while the second one does (I mean as I perceive it visually). real 0m0.006s
user 0m0.000s
sys 0m0.004s $ time python -S -c 'print "Hello World!"'
...
real 0m0.007s
user 0m0.004s
sys 0m0.003s
That's about the same speed it takes my system's cp command to show me an error page: $ time cp -fail
...
real 0m0.005s
user 0m0.003s
sys 0m0.002sSadly, both pypy and python3 are slower than python2.7. Also worth nothing that the sys time fluctates for me -- in other words when it is > 0.00s it doesn't appear to have anything to do with the command run.
That's because it has to load django and the whole django project. Here's click:
> time python test.py --help
Usage: test.py [OPTIONS]
Options:
--help Show this message and exit.
python test.py --help 0.06s user 0.02s system 95% cpu 0.078 total
and argparse: > time python test.py --help
usage: test.py [-h]
optional arguments:
-h, --help show this help message and exit
python test.py --help 0.03s user 0.02s system 93% cpu 0.054 total
reference: > time python -c 'print "hello"'
hello
python -c 'print "hello"' 0.01s user 0.01s system 88% cpu 0.025 totalAnd if you're in some project similar to Django, you would have first to query the server to know the possible subcommands...
So what you say makes sense when other parts of the code are slow to initialize and must be accessed frequently, not for the CLI itself.
edit: my test was flawed, with more accurate test there is almost a full second time difference:
$ echo -n | time ipython console --existing kernel-29793.json
1.06user 0.06system 0:01.52elapsed 74%CPU (0avgtext+0avgdata 29968maxresident)k
0inputs+64outputs (0major+19245minor)pagefaults 0swaps
$ echo -n | time ipython console
1.07user 0.08system 0:02.40elapsed 48%CPU (0avgtext+0avgdata 30264maxresident)k
0inputs+72outputs (0major+20538minor)pagefaults 0swaps
of course two seconds is ridiculously slow either wayDjango's manage command is already extensible, so if you're using Django then perhaps it's best to use its tool chain.
I'm curious about that decision given that optparse is deprecated. It says it's because argparse doesn't allow nested commands, but I don't really see why that feature is so desirable as to warrant using a deprecated module. I probably need to have a play with it to find out.
Looks like a nice module.
argparse requires the parser to have full knowledge of everything which makes it slow if you add many commands. Biggest problem though is that it's parsing system is a bit broken when it comes to escaping. Options with arguments cannot have values starting with dashes which is problematic for delegating subcommands to other things.
For what I wrote Click for I could not find any alternatives that worked that way besides optparse itself but that is hard to use.
Links to some open bugs on it:
* http://bugs.python.org/issue13966
* http://bugs.python.org/issue14191