Traits CLI is based on Enthought’s Traits library.
Some benefits:
pip install traitscli
Source code:
from traitscli import TraitsCLIBase
from traits.api import Bool, Float, Int, Str, Enum
class SampleCLI(TraitsCLIBase):
'''
Sample CLI using `traitscli`.
Example::
%(prog)s --yes # => obj.yes = True
%(prog)s --string something # => obj.string = 'string'
%(prog)s --choice x # => raise error (x is not in {a, b, c})
'''
# These variables are configurable by command line option
yes = Bool(desc='yes flag for sample CLI', config=True)
no = Bool(True, config=True)
fnum = Float(config=True)
inum = Int(config=True)
string = Str(config=True)
choice = Enum(['a', 'b', 'c'], config=True)
# You can have "internal" attributes which cannot be set via CLI.
not_configurable_from_cli = Bool()
def do_run(self):
names = self.class_trait_names(config=True)
width = max(map(len, names))
for na in names:
print "{0:{1}} : {2!r}".format(na, width, getattr(self, na))
if __name__ == '__main__':
# Run command line interface
SampleCLI.cli()
Example run:
$ python sample.py --help
usage: sample.py [-h] [--choice {a,b,c}] [--fnum FNUM] [--inum INUM] [--no]
[--string STRING] [--yes]
Sample CLI using `traitscli`.
Example::
sample.py --yes # => obj.yes = True
sample.py --string something # => obj.string = 'string'
sample.py --choice x # => raise error (x is not in {a, b, c})
optional arguments:
-h, --help show this help message and exit
--choice {a,b,c} (default: a)
--fnum FNUM (default: 0.0)
--inum INUM (default: 0)
--no (default: True)
--string STRING (default: )
--yes yes flag for sample CLI (default: False)
$ python sample.py --yes --choice a
string : ''
no : True
fnum : 0.0
choice : 'a'
inum : 0
yes : True
$ python sample.py --inum invalid_argument
usage: sample.py [-h] [--choice {a,b,c}] [--fnum FNUM] [--inum INUM] [--no]
[--string STRING] [--yes]
sample.py: error: argument --inum: invalid int value: 'invalid_argument'
CLI generator base class.
Usage. You will need to define:
Examples
To make class attribute configurable from command line options, set metadata config=True:
>>> class SampleCLI(TraitsCLIBase):
... int = Int(config=True)
...
>>> obj = SampleCLI.cli(['--int', '1'])
>>> obj.int
1
For dict and list type attribute, you can modify it using subscript access:
>>> class SampleCLI(TraitsCLIBase):
... dict = Dict(config=True)
...
>>> obj = SampleCLI.cli(['--dict["k"]', '1'])
>>> obj.dict['k']
1
You don’t need to quote string if dict/list attribute set its value trait to str-like trait:
>>> class SampleCLI(TraitsCLIBase):
... dict = Dict(value_trait=Str, config=True)
...
>>> obj = SampleCLI.cli(['--dict["k"]', 'unquoted string'])
>>> obj.dict['k']
'unquoted string'
>>> obj = SampleCLI.cli(['--dict["k"]=unquoted string'])
>>> obj.dict['k']
'unquoted string'
Attributes of nested class can be set using dot access:
>>> class SubObject(TraitsCLIBase):
... int = Int(config=True)
...
>>> class SampleCLI(TraitsCLIBase):
... # Here, ``args=()`` is required to initialize `sub`.
... sub = Instance(SubObject, args=(), config=True)
...
>>> obj = SampleCLI.cli(['--sub.int', '1'])
>>> obj.sub.int
1
Metadata for traits
If this metadata of an attribute is True, this attribute is configurable via CLI.
>>> class SampleCLI(TraitsCLIBase):
... configurable = Int(config=True)
... hidden = Int()
...
>>> with hidestderr():
... SampleCLI.cli(['--configurable', '1', '--hidden', '2'])
... # `hidden` is not configurable, so it fails:
Traceback (most recent call last):
...
SystemExit: 2
>>> obj = SampleCLI.cli(['--configurable', '1'])
>>> obj.configurable
1
>>> obj.hidden = 2
>>> obj.hidden
2
Description of this attribute. Passed to help argument of ArgumentParser.add_argument.
>>> class SampleCLI(TraitsCLIBase):
... a = Int(desc='help string for attribute a', config=True)
... b = Float(desc='help string for attribute b', config=True)
...
>>> SampleCLI.get_argparser().print_help()
usage: ... [-h] [--a A] [--b B]
optional arguments:
-h, --help show this help message and exit
--a A help string for attribute a (default: 0)
--b B help string for attribute b (default: 0.0)
If True, corresponding command line argument is interpreted as a positional argument.
>>> class SampleCLI(TraitsCLIBase):
... int = Int(cli_positional=True, config=True)
...
>>> obj = SampleCLI.cli(['1']) # no `--a` here!
>>> obj.int
1
Passed to required argument of ArgumentParser.add_argument
>>> class SampleCLI(TraitsCLIBase):
... int = Int(cli_required=True, config=True)
...
>>> with hidestderr():
... SampleCLI.cli([])
...
Traceback (most recent call last):
...
SystemExit: 2
>>> obj = SampleCLI.cli(['--int', '1'])
>>> obj.int
1
Passed to metavar argument of ArgumentParser.add_argument
>>> class SampleCLI(TraitsCLIBase):
... int = Int(cli_metavar='NUM', config=True)
...
>>> SampleCLI.get_argparser().print_help()
usage: ... [-h] [--int NUM]
optional arguments:
-h, --help show this help message and exit
--int NUM (default: 0)
This attribute has special meaning. When this metadata is True, this attribute indicate the path to parameter file The instance is first initialized using parameters defined in the parameter file, then command line arguments are used to override the parameters.
>>> class SampleCLI(TraitsCLIBase):
... int = Int(config=True)
... paramfile = Str(cli_paramfile=True, config=True)
...
>>> import json
>>> from tempfile import NamedTemporaryFile
>>> param = {'int': 1}
>>> with NamedTemporaryFile(suffix='.json') as f:
... json.dump(param, f)
... f.flush()
... obj = SampleCLI.cli(['--paramfile', f.name])
...
>>> obj.int
1
Idioms
Get a dictionary containing configurable attributes.
>>> class SampleCLI(TraitsCLIBase):
... a = Int(0, config=True)
... b = Int(1, config=True)
... c = Int(2)
...
>>> obj = SampleCLI()
>>> obj.trait_get() == {'a': 0, 'b': 1, 'c': 2}
True
>>> obj.trait_get(config=True) == {'a': 0, 'b': 1}
True
Get a list of configurable attribute names.
>>> names = SampleCLI.class_trait_names(config=True)
>>> sorted(names)
['a', 'b']
See Traits user manual for more information. Especially, Defining Traits: Initialization and Validation is useful to quickly glance traits API.
Entry points
Call run() using command line arguments.
When args is given, it is used instead of sys.argv[1:].
Essentially, the following two should do the same thing:
$ python yourcli.py --alpha 1
>>> YourCLI.run(alpha=1) # doctest: +SKIP
API to access attributes
Return configurable traits as a (possibly nested) dict.
The returned dict can be nested if this class has Instance trait of TraitsCLIBase. Use flattendict() to get a flat dictionary with dotted keys.
It is equivalent to cls.class_traits(config=True) if cls has no Instance trait.
>>> class SubObject(TraitsCLIBase):
... int = Int(config=True)
...
>>> class SampleCLI(TraitsCLIBase):
... nonconfigurable = Int()
... int = Int(config=True)
... sub = Instance(SubObject, args=(), config=True)
...
>>> traits = SampleCLI.config_traits()
>>> traits
{'int': <traits.traits.CTrait at ...>,
'sub': {'int': <traits.traits.CTrait at ...>}}
>>> traits['int'].trait_type
<traits.trait_types.Int object at ...>
>>> traits['sub']['int'].trait_type
<traits.trait_types.Int object at ...>
Set attribute given a dictionary attrs.
Keys of attrs can be dot-separated name (e.g., a.b.c). In this case, nested attribute will be set to its attribute.
The values of attrs can be a dict. If the corresponding attribute is an instance of TraitsCLIBase, attributes of this instance is set using this dictionary. Otherwise, it will issue an error.
>>> obj = TraitsCLIBase()
>>> obj.b = TraitsCLIBase()
>>> obj.setattrs({'a': 1, 'b': {'c': 2}})
>>> obj.a
1
>>> obj.b.c
2
>>> obj.setattrs({'b.a': 111, 'b.c': 222})
>>> obj.b.a
111
>>> obj.b.c
222
>>> obj.setattrs({'x.a': 0})
Traceback (most recent call last):
...
AttributeError: 'TraitsCLIBase' object has no attribute 'x'
If only_configurable is True, attempt to set non-configurable attributes raises an error.
>>> class SampleCLI(TraitsCLIBase):
... a = Int(config=True)
... b = Int()
...
>>> obj = SampleCLI()
>>> obj.setattrs({'a': 1}, only_configurable=True) # This is OK.
>>> obj.setattrs({'b': 1}, only_configurable=True) # This is not!
Traceback (most recent call last):
...
TraitsCLIAttributeError: Non-configurable key is given: b
Load attributes from parameter file at path.
To support new parameter file, add a class-method called loader_{ext} where {ext} is the file extension of the parameter file. You can also redefine dispatch_paramfile_loader class-method to change how loader function is chosen.
>>> from tempfile import NamedTemporaryFile
>>> class SampleCLI(TraitsCLIBase):
... int = Int(config=True)
...
>>> obj = SampleCLI()
>>> with NamedTemporaryFile(suffix='.json') as f:
... f.write('{"int": 1}')
... f.flush()
... obj.load_paramfile(f.name)
...
>>> obj.int
1
You can use only_configurable=False to set non-configurable option.
>>> obj = TraitsCLIBase()
>>> with NamedTemporaryFile(suffix='.json') as f:
... f.write('{"nonconfigurable": 1}')
... f.flush()
... obj.load_paramfile(f.name, only_configurable=False)
...
>>> obj.nonconfigurable
1
Load attributes from all parameter files set in paramfile attributes.
Path of parameter file is defined by attributes whose metadata cli_paramfile is True.
>>> from tempfile import NamedTemporaryFile
>>> from contextlib import nested
>>> class SampleCLI(TraitsCLIBase):
... int = Int(config=True)
... str = Str(config=True)
... paramfiles = List(cli_paramfile=True, config=True)
...
>>> obj = SampleCLI()
>>> with nested(NamedTemporaryFile(suffix='.json'),
... NamedTemporaryFile(suffix='.json')) as (f, g):
... f.write('{"int": 1}')
... f.flush()
... g.write('{"str": "a"}')
... g.flush()
... obj.paramfiles = [f.name, g.name]
... obj.load_all_paramfiles()
...
>>> obj.int
1
>>> obj.str
u'a'
Return an parameter file loader function based on path.
This classmethod returns classmethod/staticmethod named laoder_{ext} where {ext} is the file extension of path. You can redefine this classmethod to change the dispatching behavior.
Call signature of the loader function must be loader(path) where path is a string file path to the parameter file.
Load JSON file located at path.
It is equivalent to json.load(open(path)).
Load YAML file located at path.
It is equivalent to yaml.load(open(path)). You need PyYAML module to use this loader.
Alias to loader_yaml().
Load parameter from conf/ini file.
As conf file has no type information, class traits will be used at load time.
>>> class SubObject(TraitsCLIBase):
... c = Int(config=True)
>>> class SampleCLI(TraitsCLIBase):
... a = Int(config=True)
... b = Instance(SubObject, args=(), config=True)
... d = Instance(SubObject, args=(), config=True)
... cli_conf_root_section = 'root' # this is default
You can write options using dot-separated name. Use the section specified by cli_conf_root_section for top-level attributes.
>>> from tempfile import NamedTemporaryFile
>>> source = '''
... [root]
... a = 1
... b.c = 2
... '''
>>> with NamedTemporaryFile() as f:
... f.write(source)
... f.flush()
... param = SampleCLI.loader_conf(f.name)
>>> param == {'a': 1, 'b.c': 2}
True
Options in sections other than cli_conf_root_section are prefixed by section name.
>>> from tempfile import NamedTemporaryFile
>>> source = '''
... [root]
... a = 1
... [b]
... c = 2
... [d]
... c = 3
... '''
>>> with NamedTemporaryFile() as f:
... f.write(source)
... f.flush()
... param = SampleCLI.loader_conf(f.name)
>>> param == {'a': 1, 'b.c': 2, 'd.c': 3}
True
Alias to loader_conf().
Root section name for conf/ini file loader (loader_conf()).
Options in this section will not be prefixed by section name.
Load parameter from Python file located at path.
>>> from tempfile import NamedTemporaryFile
>>> source = '''
... a = 1
... b = dict(c=2)
... _underscored_value_ = 'will_not_be_loaded'
... '''
>>> with NamedTemporaryFile() as f:
... f.write(source)
... f.flush()
... param = TraitsCLIBase.loader_py(f.name)
>>> param == {'a': 1, 'b': {'c': 2}}
True
Parser API
Argument parser class/factory.
This attribute must be a callable object which returns an instance of argparse.ArgumentParser or its subclass.
Return an instance of ArgumentParser for this class.
Parser options are set according to the configurable traits of this class.
Call parser.add_argument based on class traits of cls.
This classmethod is called from get_argparser().
Launch CLI to call multiple classes.
Usage:
>>> class SampleBase(TraitsCLIBase):
... a = Int(config=True)
... def do_run(self):
... print "Running",
... print '{0}(a={1!r})'.format(self.__class__.__name__,
... self.a)
...
>>> class SampleInit(SampleBase):
... pass
...
>>> class SampleCheckout(SampleBase):
... pass
...
>>> class SampleBranch(SampleBase):
... pass
...
>>> obj = multi_command_cli(
... # CLI classes and subcommand names
... [('init', SampleInit),
... ('checkout', SampleCheckout),
... ('branch', SampleBranch),
... ],
... # Command line arguments
... ['init', '--a', '1'])
...
Running SampleInit(a=1)
>>> isinstance(obj, SampleInit) # used CLI object is returned.
True
If ArgumentParser is not specified, ArgumentParser of the first class will be used.
Flatten dictionary using key concatenated by dot.
>>> flattendict({'a': 1, 'b': 2}) == {'a': 1, 'b': 2}
True
>>> flattendict({'a': 1, 'b': {'c': 2}}) == {'a': 1, 'b.c': 2}
True