Traits CLI - CLI generator based on class traits

Traits CLI is based on Enthought’s Traits library.

Some benefits:

  • Automatically set type (int/float/...) of command line argument.
  • Help string generation.
  • “Deep value”” configuration: e.g., --dict['a']['b']['c']=1 is equivalent to obj.dict['a']['b']['c'] = 1 in Python code.
  • Nested class configuration: e.g., --sub.attr=val is equivalent to obj.sub.attr = val in Python code.
  • Parameter file support (ini/conf, json, yaml, etc.). Load parameter from file then set attribute.

Installation

pip install traitscli

Dependencies

  • traits
  • argparse (for Python < 2.7)

Sample

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 base class

class traitscli.TraitsCLIBase(**kwds)

CLI generator base class.

Usage. You will need to define:

  1. Parameters (traits). When it has config=True metadata, it is configurable via command line argument. See: Defining Traits: Initialization and Validation section in Traits user manual.
  2. do_run() method. This method gets no argument (except self). Do whatever this class needs to do based on its attributes. cli() function sets attributes based on command line options and then call do_run() method.

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

config : bool

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
desc : string

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)
cli_positional : bool

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
cli_required : bool

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
cli_metavar : str

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)
cli_paramfile : bool

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

classmethod cli(args=None)

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
classmethod run(**kwds)

Make an instance with args kwds and call do_run().

do_run()

Actual implementation of run().

Child class must implement this method.

API to access attributes

classmethod config_traits(**metadata)

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 ...>
setattrs(attrs, only_configurable=False)

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_paramfile(path, only_configurable=True)

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_all_paramfiles()

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'
classmethod dispatch_paramfile_loader(path)

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.

static loader_json(path, _open=<built-in function open>)

Load JSON file located at path.

It is equivalent to json.load(open(path)).

static loader_yaml(path, _open=<built-in function open>)

Load YAML file located at path.

It is equivalent to yaml.load(open(path)). You need PyYAML module to use this loader.

static loader_yml(path, _open=<built-in function open>)

Alias to loader_yaml().

classmethod loader_conf(path, _open=<built-in function open>)

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
classmethod loader_ini(path, _open=<built-in function open>)

Alias to loader_conf().

cli_conf_root_section = 'root'

Root section name for conf/ini file loader (loader_conf()).

Options in this section will not be prefixed by section name.

static loader_py(path, _open=<built-in function open>)

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

ArgumentParser = <class 'argparse.ArgumentParser'>

Argument parser class/factory.

This attribute must be a callable object which returns an instance of argparse.ArgumentParser or its subclass.

classmethod get_argparser()

Return an instance of ArgumentParser for this class.

Parser options are set according to the configurable traits of this class.

classmethod add_parser(parser, prefix='')

Call parser.add_argument based on class traits of cls.

This classmethod is called from get_argparser().

Utility functions

traitscli.multi_command_cli(command_class_pairs, args=None, ArgumentParser=None)

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.

traitscli.flattendict(dct)

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

Change log

v0.1

  • Classes which inherits HasTraits but does not inherit TraitsCLIBase also can be used as a configurable trait.

Project Versions

Table Of Contents

This Page