Command Tree’s documentation¶
Summary¶
The command-tree is a lightweight framework to build multi level command line interfaces by using the python builtin argparse module.
- The main objectives are:
- Full
argparse.ArgumentParser
compatibility. - Use as litle as possible code to build the tree, but preserve the argparse flexibiltiy.
- The class and function structure must looks as the cli tree.
- Decorators, decorators everywhere. We love decorators, so we use as often as possible.
- Full
Usage¶
To understand how to use it see this example first: Basic example. This page contains an example implemented in 2 ways: one with argparse and one with command-tree.
There is a page where we dissected the basic command-tree example and add a lot of comment to explains the code: Basic example with a lot of comments
Config¶
Some very cool extra features can be configured via the Config class. For further information see command_tree.config.Config
.
For a very simple example see Basic config example.
Examples¶
Basic example¶
argparse¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | from argparse import ArgumentParser
parser = ArgumentParser()
subparsers1 = parser.add_subparsers(dest = 'subcommand')
command1parser = subparsers1.add_parser('command1')
command1parser.add_argument("arg1")
def command1_handler(args):
return int(args.arg1) / 2
command1parser.set_defaults(func = command1_handler)
command2parser = subparsers1.add_parser('command2')
command2parser.add_argument("arg1")
def command2_handler(args):
return int(args.arg1) * 2
command2parser.set_defaults(func = command2_handler)
args = parser.parse_args()
print(args.func(args))
|
command-tree¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from command_tree import CommandTree
tree = CommandTree()
@tree.root()
class Root(object):
@tree.leaf()
@tree.argument()
def command1(self, arg1):
return int(arg1) / 2
@tree.leaf()
@tree.argument()
def command2(self, arg1):
return int(arg1) * 2
print(tree.execute())
|
Basic example with a lot of comments¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | # import the CommandTree. The import is important.
from command_tree import CommandTree
# Create the CommandTree instance. This is mandatory. Every decorator must be used from
# this instance.
tree = CommandTree()
# The root decorator is mandatory. Use in the top of the tree, at the root class.
# Only one root allowed per CommandTree instance. This a special, the top-level node.
@tree.root()
# This is a handler class. Must be derived from object, but no other ancestor is
# neccessary.
class Root(object):
# Constructor is not neccessary if there is no argument for the node,
# but may have if you want to initialize your personal stuffs.
# Mark this function as a leaf. Must be used under a node. By default the function
# name used as parser name. Every parameter in the leaf arguments are passed
# to the ArgumentParser ctor.
@tree.leaf()
# We have an argument here! IMPORTANT: you have to use the argument decoator
# as many as argument has the handler fuction. (this case: command1)
# All positional and keyword arguments are passed to ArgumentParser.add_argument
# function
@tree.argument()
# The leaf's handler function. When the user execute the `script.py command1 42` the
# command-tree will call this function (after instantiate the parent node classes)
def command1(self, arg1):
# this return value will be returned by the CommandTree.execute
return int(arg1) / 2
@tree.leaf()
@tree.argument()
def command2(self, arg1):
return int(arg1) * 2
# After you built the tree try to execute. The CommandTree will build the argparse tree,
# call the ArgumentParser.parse_args and search for the selected handler.
print(tree.execute())
|
Commands in files¶
This example shows how to distribute your code if you want to separate the node handler classes to external files.
1 2 3 | from command_tree import CommandTree
tree = CommandTree()
|
1 2 3 4 5 6 7 8 9 | from tree import tree
@tree.node()
class Node1(object):
@tree.leaf()
@tree.argument()
def divide(self, arg1):
return int(arg1) / 2
|
1 2 3 4 5 6 7 8 9 | from tree import tree
@tree.node()
class Node2(object):
@tree.leaf()
@tree.argument()
def multiply(self, arg1):
return int(arg1) * 2
|
1 2 3 4 5 6 | from tree import tree
@tree.leaf()
@tree.argument()
def power(self, arg1):
return int(arg1) * int(arg1)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from tree import tree
from node1 import Node1
from node2 import Node2
from power import power as Power
@tree.root()
class Root(object):
node1 = Node1
node2 = Node2
power = Power
print(tree.execute())
|
Basic config example¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from command_tree import CommandTree, Config
config = Config(change_underscores_to_hyphens_in_names = True)
tree = CommandTree(config)
@tree.root()
class Root(object):
@tree.leaf()
def command_one(self):
return 42
print(tree.execute())
|
Groups¶
Argument groups¶
It has been implement the simple argument group as described as argparse.ArgumentParser.add_argument_group()
.
The parameters of the command_tree.groups.ArgumentGroup
are the exact same like the argparse one.
Usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from command_tree import CommandTree, ArgumentGroup
tree = CommandTree()
@tree.root()
class Root(object):
grp1 = ArgumentGroup(tree, "platypus")
@tree.leaf()
@grp1.argument("--foo")
@grp1.argument("--bar")
def add(self, foo = 42, bar = 21):
return foo + bar
print(tree.execute())
|
Result:
$ python groups/arg_group.py add -h
usage: arg_group.py add [-h] [--foo FOO] [--bar BAR]
optional arguments:
-h, --help show this help message and exit
platypus:
--foo FOO
--bar BAR
Mutual exclusion¶
It has been implement the mutually exclusive argument group as described as argparse.ArgumentParser.add_mutually_exclusive_group()
.
The parameters of the command_tree.groups.MutuallyExclusiveGroup
are the exact same like the argparse one.
Usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from command_tree import CommandTree, MutuallyExclusiveGroup
tree = CommandTree()
@tree.root()
class Root(object):
grp1 = MutuallyExclusiveGroup(tree, required = True)
@tree.leaf()
@grp1.argument("--foo")
@grp1.argument("--bar")
def add(self, foo = 42, bar = 21):
return foo + bar
print(tree.execute())
|
Result:
$ python groups/mutex.py add --foo 1 --bar 2
usage: mutex.py add [-h] (--foo FOO | --bar BAR)
mutex.py add: error: argument --bar: not allowed with argument --foo
Mutex group in argument group¶
If you want to add a mutex group into an argument group, it’s possible:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from command_tree import CommandTree, ArgumentGroup, MutuallyExclusiveGroup
tree = CommandTree()
@tree.root()
class Root(object):
arg_grp = ArgumentGroup(tree, "platypus")
mutex = MutuallyExclusiveGroup(tree, required = True, argument_group = arg_grp)
@tree.leaf()
@mutex.argument("--foo")
@mutex.argument("--bar")
def add(self, foo = 42, bar = 21):
return foo + bar
print(tree.execute())
|
Parsing the docstring for help¶
Google (default) format¶
The command-tree by default can parse the classes and function docstring for search help for commands and arguments. The default comment format defined by the Google. For more info, see https://google.github.io/styleguide/pyguide.html#Comments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from command_tree import CommandTree
tree = CommandTree()
@tree.root()
class Root(object):
@tree.leaf()
@tree.argument()
def command1(self, arg1):
"""Help for command1
Args:
arg1: help for arg1
"""
return int(arg1) / 2
@tree.leaf()
@tree.argument()
def command2(self, arg1):
"""Help for command2
Args:
arg1: help for arg1
"""
return int(arg1) * 2
print(tree.execute())
|
python examples/help.py -h
usage: help.py [-h] subcommand ...
positional arguments:
subcommand
command1 Help for command1
command2 Help for command2
optional arguments:
-h, --help show this help message and exit
python examples/help.py command1 -h
usage: help.py command1 [-h] arg1
positional arguments:
arg1 help for arg1
optional arguments:
-h, --help show this help message and exit
Custom format¶
But if you want to use an other comment format, you can specify a custom comment parser in the config:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | from command_tree import CommandTree, Config
from command_tree.doc_string_parser import DocStringInfo, ParserBase
class MyDocStringParser(ParserBase):
def parse(self, content):
info = DocStringInfo()
# parse the content and put into a DocStringInfo instance ...
return info
config = Config(docstring_parser = MyDocStringParser())
tree = CommandTree(config)
@tree.root()
class Root(object):
@tree.leaf()
@tree.argument()
def command1(self, arg1):
"""Help for command1
Parameters
----------
arg1 : int
Description of arg1
"""
return int(arg1) / 2
print(tree.execute())
|
The node handler¶
When you want to do something (eg prints current version) but don’t want to add a command for it. For example cli -v
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from command_tree import CommandTree
tree = CommandTree()
@tree.optional
@tree.root()
@tree.argument('-v', action = 'store_true')
class Root(object):
def __init__(self, version):
pass
@tree.leaf()
def command1(self):
return "1"
@tree.node_handler
def handler(self, version):
if version:
return "42.0"
print(tree.execute())
|
Other¶
Alternatives¶
Before we started to develop the command-tree we searched for other solutions but did not found a good alternative.
argparse¶
- pro:
- builtin
- contra:
- very low-level
- needs lots of code to build a tree
click¶
- https://github.com/pallets/click
- pro:
- ~3800 star
- nested arguments (like argparser’s subparsers)
- very richfull
- contra:
- build a nested struct with flat struct
- positional arguments cannot have help (“Arguments cannot be documented this way. This is to follow the general convention of Unix tools of using arguments for only the most necessary things and to document them in the introduction text by referring to them by name.”)
- not argparse compatible
docopt¶
- https://github.com/docopt/docopt
- pro:
- ~4800 star
- multilang
- contra:
- no support for multi level commands
arghandler¶
- https://github.com/druths/arghandler
- contra:
- wut