scriptconfig package

Submodules

Module contents

ScriptConfig

The goal of scriptconfig is to make it easy to be able to define a CLI by simply defining a dictionary. Thie enables you to write simple configs and update from CLI, kwargs, and/or json.

The pattern is simple:

  1. Create a class that inherits from scriptconfig.Config

  2. Create a class variable dictionary named default

  3. The keys are the names of your arguments, and the values are the defaults.

  4. Create an instance of your config object. If you pass cmdline=True as an argument, it will autopopulate itself from the command line.

Here is an example for a simple calculator program:

import scriptconfig as scfg


class MyConfig(scfg.Config):
    'The docstring becomes the CLI description!'
    default = {
        'num1': 0,
        'num2': 1,
        'outfile': './result.txt',
    }


def main():
    config = MyConfig(cmdline=True)
    result = config['num1'] + config['num2']
    with open(config['outfile'], 'w') as file:
        file.write(str(result))


if __name__ == '__main__':
    main()

If the above is written to a file calc.py, it can be be called like this.

python calc.py --num1=3 --num2=4 --outfile=/dev/stdout

It is possible to gain finer control over the CLI by specifying the values in default as a scriptconfig.Value, where you can specify a help message, the expected variable type, if it is a positional variable, alias parameters for the command line, and more.

The important thing that gives scriptconfig an edge over things like argparse is that it is trivial to disable the cmdline flag and pass explicit arguments into your function as a dictionary. Thus you can write you scripts in such a way that they are callable from Python or from a CLI via with an API that corresponds 1-to-1!

A more complex example version of the above code might look like this

import scriptconfig as scfg


class MyConfig(scfg.Config):
    '''
    The docstring becomes the CLI description!
    '''
    default = {
        'num1': scfg.Value(0, type=float, help='first number to add', position=1),
        'num2': scfg.Value(1, type=float, help='second number to add', position=2),
        'outfile': scfg.Value('./result.txt', help='where to store the result', position=3),
    }


def main(cmdline=1, **kwargs):
    '''
    Example:
        >>> # This is much easier to test than argparse code
        >>> kwargs = {'num1': 42, 'num2': 23, 'outfile': 'foo.out'}
        >>> cmdline = 0
        >>> main(cmdline=cmdline, **kwargs)
        >>> with open('foo.out') as file:
        >>>     assert file.read() == '65'
    '''
    config = MyConfig(cmdline=True, data=kwargs)
    result = config['num1'] + config['num2']
    with open(config['outfile'], 'w') as file:
        file.write(str(result))


if __name__ == '__main__':
    main()

This code can be called with positional arguments:

python calc.py 33 44 /dev/stdout

The help text for this program (via python calc.py --help) looks like this:

usage: MyConfig [-h] [--num1 NUM1] [--num2 NUM2] [--outfile OUTFILE] [--config CONFIG] [--dump DUMP] [--dumps] num1 num2 outfile

The docstring becomes the CLI description!

positional arguments:
  num1               first number to add
  num2               second number to add
  outfile            where to store the result

optional arguments:
  -h, --help         show this help message and exit
  --num1 NUM1        first number to add (default: 0)
  --num2 NUM2        second number to add (default: 1)
  --outfile OUTFILE  where to store the result (default: ./result.txt)
  --config CONFIG    special scriptconfig option that accepts the path to a on-disk configuration file, and loads that into this 'MyConfig' object. (default: None)
  --dump DUMP        If specified, dump this config to disk. (default: None)
  --dumps            If specified, dump this config stdout (default: False)

Note that keyword arguments are always available, even if the argument is marked as positional. This is because a scriptconfig object always reduces to key/value pairs — i.e. a dictionary.

See the scriptconfig.config module docs for details and examples on getting started as well as getting_started docs

class scriptconfig.Config(data=None, default=None, cmdline=False, _dont_call_post_init=False)[source]

Bases: NiceRepr, DictLike

Base class for custom configuration objects

A configuration that can be specified by commandline args, a yaml config file, and / or a in-code dictionary. To use, define a class variable named __default__ and passing it to a dict of default values. You can also use special Value classes to denote types. You can also define a method __post_init__, to postprocess the arguments after this class receives them.

Basic usage is as follows.

Create a class that inherits from this class.

Assign the “__default__” class-level variable as a dictionary of options

The keys of this dictionary must be command line friendly strings.

The values of the “defaults dictionary” can be literal values or instances of the scriptconfig.Value class, which allows for specification of default values, type information, help strings, and aliases.

You may also implement __post_init__ (function with that takes no args and has no return) to postprocess your results after initialization.

When creating an instance of the class the defaults variable is used to make a dictionary-like object. You can override defaults by specifying the data keyword argument to either a file path or another dictionary. You can also specify cmdline=True to allow the contents of sys.argv to influence the values of the new object.

An instance of the config class behaves like a dictionary, except that you cannot set keys that do not already exist (as specified in the defaults dict).

Key Methods:

  • dump - dump a json representation to a file

  • dumps - dump a json representation to a string

  • argparse - create an argparse.ArgumentParser object that is defined by the defaults of this config.

  • load - rewrite the values based on a filepath, dictionary, or command line contents.

Variables:
  • _data – this protected variable holds the raw state of the config object and is accessed by the dict-like

  • _default – this protected variable maintains the default values for this config.

  • epilog (str) – A class attribute that if specified will add an epilog section to the help text.

Example

>>> # Inherit from `Config` and assign `__default__`
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __default__ = {
>>>         'option1': scfg.Value((1, 2, 3), tuple),
>>>         'option2': 'bar',
>>>         'option3': None,
>>>     }
>>> # You can now make instances of this class
>>> config1 = MyConfig()
>>> config2 = MyConfig(default=dict(option1='baz'))
Parameters:
  • data (object) – filepath, dict, or None

  • default (dict | None) – overrides the class defaults

  • cmdline (bool | List[str] | str | dict) – If False, then no command line information is used. If True, then sys.argv is parsed and used. If a list of strings that used instead of sys.argv. If a string, then that is parsed using shlex and used instead

    of sys.argv.

    If a dictionary grants fine grained controls over the args passed to Config._read_argv(). Can contain:

    • strict (bool): defaults to False

    • argv (List[str]): defaults to None

    • special_options (bool): defaults to True

    • autocomplete (bool): defaults to False

    Defaults to False.

Note

Avoid setting cmdline parameter here. Instead prefer to use the cli classmethod to create a command line aware config instance..

classmethod cli(data=None, default=None, argv=None, strict=True, cmdline=True, autocomplete='auto')[source]

Create a commandline aware config instance.

Calls the original “load” way of creating non-dataclass config objects. This may be refactored in the future.

Parameters:
  • data (dict | str | None) – Values to update the configuration with. This can be a regular dictionary or a path to a yaml / json file.

  • default (dict | None) – Values to update the defaults with (not the actual configuration). Note: anything passed to default will be deep copied and can be updated by argv or data if it is specified. Generally prefer to pass directly to data instead.

  • cmdline (bool) – Defaults to True, which creates and uses an argparse object to interact with the command line. If set to False, then the argument parser is bypassed (useful for invoking a CLI programatically with kwargs and ignoring sys.argv).

  • argv (List[str]) – if specified, ignore sys.argv and parse this instead.

  • strict (bool) – if True use parse_args otherwise use parse_known_args. Defaults to True.

  • autocomplete (bool | str) – if True try to enable argcomplete.

classmethod demo()[source]

Create an example config class for test cases

CommandLine

xdoctest -m scriptconfig.config Config.demo
xdoctest -m scriptconfig.config Config.demo --cli --option1 fo

Example

>>> from scriptconfig.config import *
>>> self = Config.demo()
>>> print('self = {}'.format(self))
self = <DemoConfig({...'option1': ...}...)...>...
>>> self.argparse().print_help()
>>> # xdoc: +REQUIRES(--cli)
>>> self.load(cmdline=True)
>>> print(ub.urepr(self, nl=1))
getitem(key)[source]

Dictionary-like method to get the value of a key.

Parameters:

key (str) – the key

Returns:

the associated value

Return type:

Any

setitem(key, value)[source]

Dictionary-like method to set the value of a key.

Parameters:
  • key (str) – the key

  • value (Any) – the new value

delitem(key)[source]
keys()[source]

Dictionary-like keys method

Yields:

str

update_defaults(default)[source]

Update the instance-level default values

Parameters:

default (dict) – new defaults

load(data=None, cmdline=False, mode=None, default=None, strict=False, autocomplete=False, _dont_call_post_init=False)[source]

Updates the configuration from a given data source.

Any option can be overwritten via the command line if cmdline is truthy.

Parameters:
  • data (PathLike | dict) – Either a path to a yaml / json file or a config dict

  • cmdline (bool | List[str] | str | dict) – If False, then no command line information is used. If True, then sys.argv is parsed and used. If a list of strings that used instead of sys.argv. If a string, then that is parsed using shlex and used instead

    of sys.argv.

    If a dictionary grants fine grained controls over the args passed to Config._read_argv(). Can contain:

    • strict (bool): defaults to False

    • argv (List[str]): defaults to None

    • special_options (bool): defaults to True

    • autocomplete (bool): defaults to False

    Defaults to False.

  • mode (str | None) – Either json or yaml.

  • cmdline (bool | List[str] | str) – If False, then no command line information is used. If True, then sys.argv is parsed and used. If a list of strings that used instead of sys.argv. If a string, then that is parsed using shlex and used instead of sys.argv. Defaults to False.

  • default (dict | None) – updated defaults. Note: anything passed to default will be deep copied and can be updated by argv or data if it is specified. Generally prefer to pass directly to data instead.

  • strict (bool) – if True an error will be raised if the command line contains unknown arguments.

  • autocomplete (bool) – if True, attempts to use the autocomplete package if it is available if reading from sys.argv. Defaults to False.

Note

if cmdline=True, this will create an argument parser.

Example

>>> # Test load works correctly in cmdline True and False mode
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __default__ = {
>>>         'src': scfg.Value(None, help=('some help msg')),
>>>     }
>>> data = {'src': 'hi'}
>>> self = MyConfig(data=data, cmdline=False)
>>> assert self['src'] == 'hi'
>>> self = MyConfig(default=data, cmdline=True)
>>> assert self['src'] == 'hi'
>>> # In 0.5.8 and previous src fails to populate!
>>> # This is because cmdline=True overwrites data with defaults
>>> self = MyConfig(data=data, cmdline=True)
>>> assert self['src'] == 'hi', f'Got: {self}'

Example

>>> # Test load works correctly in strict mode
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __default__ = {
>>>         'src': scfg.Value(None, help=('some help msg')),
>>>     }
>>> data = {'src': 'hi'}
>>> cmdlinekw = {
>>>     'strict': True,
>>>     'argv': '--src=hello',
>>> }
>>> self = MyConfig(data=data, cmdline=cmdlinekw)
>>> cmdlinekw = {
>>>     'strict': True,
>>>     'special_options': False,
>>>     'argv': '--src=hello --extra=arg',
>>> }
>>> import pytest
>>> with pytest.raises(SystemExit):
>>>     self = MyConfig(data=data, cmdline=cmdlinekw)

Example

>>> # Test load works correctly with required
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __default__ = {
>>>         'src': scfg.Value(None, help=('some help msg'), required=True),
>>>     }
>>> cmdlinekw = {
>>>     'strict': True,
>>>     'special_options': False,
>>>     'argv': '',
>>> }
>>> import pytest
>>> with pytest.raises(Exception):
...   self = MyConfig(cmdline=cmdlinekw)

Example

>>> # Test load works correctly with alias
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __default__ = {
>>>         'opt1': scfg.Value(None),
>>>         'opt2': scfg.Value(None, alias=['arg2']),
>>>     }
>>> config1 = MyConfig(data={'opt2': 'foo'})
>>> assert config1['opt2'] == 'foo'
>>> config2 = MyConfig(data={'arg2': 'bar'})
>>> assert config2['opt2'] == 'bar'
>>> assert 'arg2' not in config2
_normalize_alias_key(key)[source]

normalizes a single aliased key

_normalize_alias_dict(data)[source]
Parameters:

data (dict) – dictionary with keys that could be aliases

Returns:

keys are normalized to be primary keys.

Return type:

dict

_build_alias_map()[source]
_read_argv(argv=None, special_options=True, strict=False, autocomplete=False)[source]

Example

>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     description = 'my CLI description'
>>>     __default__ = {
>>>         'src':  scfg.Value(['foo'], position=1, nargs='+'),
>>>         'dry':  scfg.Value(False),
>>>         'approx':  scfg.Value(False, isflag=True, alias=['a1', 'a2']),
>>>     }
>>> self = MyConfig()
>>> # xdoctest: +REQUIRES(PY3)
>>> # Python2 argparse does a hard sys.exit instead of raise
>>> import sys
>>> if sys.version_info[0:2] < (3, 6):
>>>     # also skip on 3.5 because of dict ordering
>>>     import pytest
>>>     pytest.skip()
>>> self._read_argv(argv='')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='--src [,]')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='--src [,] --a1')
>>> print('self = {}'.format(self))
self = <MyConfig({'src': ['foo'], 'dry': False, 'approx': False})>
self = <MyConfig({'src': [], 'dry': False, 'approx': False})>
self = <MyConfig({'src': [], 'dry': False, 'approx': True})>
>>> self = MyConfig()
>>> self._read_argv(argv='p1 p2 p3')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='--src=p4,p5,p6!')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='p1 p2 p3 --src=p4,p5,p6!')
>>> print('self = {}'.format(self))
self = <MyConfig({'src': ['p1', 'p2', 'p3'], 'dry': False, 'approx': False})>
self = <MyConfig({'src': ['p4', 'p5', 'p6!'], 'dry': False, 'approx': False})>
self = <MyConfig({'src': ['p4', 'p5', 'p6!'], 'dry': False, 'approx': False})>
>>> self = MyConfig()
>>> self._read_argv(argv='p1')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='--src=p4')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='p1 --src=p4')
>>> print('self = {}'.format(self))
self = <MyConfig({'src': ['p1'], 'dry': False, 'approx': False})>
self = <MyConfig({'src': ['p4'], 'dry': False, 'approx': False})>
self = <MyConfig({'src': ['p4'], 'dry': False, 'approx': False})>
>>> special_options = False
>>> parser = self.argparse(special_options=special_options)
>>> parser.print_help()
>>> x = parser.parse_known_args()
dump(stream=None, mode=None)[source]

Write configuration file to a file or stream

Parameters:
  • stream (FileLike | None) – the stream to write to

  • mode (str | None) – can be ‘yaml’ or ‘json’ (defaults to ‘yaml’)

dumps(mode=None)[source]

Write the configuration to a text object and return it

Parameters:

mode (str | None) – can be ‘yaml’ or ‘json’ (defaults to ‘yaml’)

Returns:

str - the configuration as a string

property _description
property _epilog
property _prog
_parserkw()[source]

Generate the kwargs for making a new argparse.ArgumentParser

port_to_dataconf()[source]

Helper that will write the code to express this config as a DataConfig.

CommandLine

xdoctest -m scriptconfig.config Config.port_to_dataconf

Example

>>> import scriptconfig as scfg
>>> self = scfg.Config.demo()
>>> print(self.port_to_dataconf())
classmethod _write_code(entries, name='MyConfig', style='dataconf', description=None)[source]
classmethod port_click(click_main, name='MyConfig', style='dataconf')[source]

Example

@click.command() @click.option(’–dataset’, required=True, type=click.Path(exists=True), help=’input dataset’) @click.option(’–deployed’, required=True, type=click.Path(exists=True), help=’weights file’) def click_main(dataset, deployed):

classmethod port_argparse(parser, name='MyConfig', style='dataconf')[source]

Generate the corresponding scriptconfig code from an existing argparse instance.

Parameters:
  • parser (argparse.ArgumentParser) – existing argparse parser we want to port

  • name (str) – the name of the config class

  • style (str) – either ‘orig’ or ‘dataconf’

Returns:

code to create a scriptconfig object that should work similarly to the existing argparse object.

Return type:

str

Note

The correctness of this function is not guarenteed. This only works perfectly in simple cases, but in complex cases it may not produce 1-to-1 results, however it will provide a useful starting point.

Todo

  • [X] Handle “store_true”.

  • [ ] Argument groups.

  • [ ] Handle mutually exclusive groups

Example

>>> import scriptconfig as scfg
>>> import argparse
>>> parser = argparse.ArgumentParser(description='my argparse')
>>> parser.add_argument('pos_arg1')
>>> parser.add_argument('pos_arg2', nargs='*')
>>> parser.add_argument('-t', '--true_dataset', '--test_dataset', help='path to the groundtruth dataset', required=True)
>>> parser.add_argument('-p', '--pred_dataset', help='path to the predicted dataset', required=True)
>>> parser.add_argument('--eval_dpath', help='path to dump results')
>>> parser.add_argument('--draw_curves', default='auto', help='flag to draw curves or not')
>>> parser.add_argument('--score_space', default='video', help='can score in image or video space')
>>> parser.add_argument('--workers', default='auto', help='number of parallel scoring workers')
>>> parser.add_argument('--draw_workers', default='auto', help='number of parallel drawing workers')
>>> group1 = parser.add_argument_group('mygroup1')
>>> group1.add_argument('--group1_opt1', action='store_true')
>>> group1.add_argument('--group1_opt2')
>>> group2 = parser.add_argument_group()
>>> group2.add_argument('--group2_opt1', action='store_true')
>>> group2.add_argument('--group2_opt2')
>>> mutex_group3 = parser.add_mutually_exclusive_group()
>>> mutex_group3.add_argument('--mgroup3_opt1')
>>> mutex_group3.add_argument('--mgroup3_opt2')
>>> text = scfg.Config.port_argparse(parser, name='PortedConfig', style='dataconf')
>>> print(text)
>>> # Make an instance of the ported class
>>> vals = {}
>>> exec(text, vals)
>>> cls = vals['PortedConfig']
>>> self = cls(**{'true_dataset': 1, 'pred_dataset': 1})
>>> recon = self.argparse()
>>> print('recon._actions = {}'.format(ub.urepr(recon._actions, nl=1)))
port_to_argparse()[source]

Attempt to make code for a nearly-equivalent argparse object.

This code only handles basic cases. Some of the scriptconfig magic is dropped so we dont need to rely on custom actions.

The idea is that sometimes we can’t depend on scriptconfig, so it would be nice to be able to translate an existing scriptconfig class to the nearly equivalent argparse code.

SeeAlso:

Config.argparse() - creates a real argparse object

Returns:

code to construct a similar argparse object

Return type:

str

CommandLine

xdoctest -m scriptconfig.config Config.port_to_argparse

Example

>>> import scriptconfig as scfg
>>> class SimpleCLI(scfg.DataConfig):
>>>     data = scfg.Value(None, help='input data', position=1)
>>> self_or_cls = SimpleCLI()
>>> text = self_or_cls.port_to_argparse()
>>> print(text)
>>> # Test that the generated code is executable
>>> ns = {}
>>> exec(text, ns, ns)
>>> parser = ns['parser']
>>> args1 = parser.parse_args(['foobar'])
>>> assert args1.data == 'foobar'
>>> # Looks like we cant do positional or key/value easilly
>>> #args1 = parser.parse_args(['--data=blag'])
>>> #print('args1 = {}'.format(ub.urepr(args1, nl=1)))
property namespace

Access a namespace like object for compatibility with argparse

Returns:

argparse.Namespace

to_omegaconf()[source]

Creates an omegaconfig version of this.

Return type:

omegaconf.OmegaConf

Example

>>> # xdoctest: +REQUIRES(module:omegaconf)
>>> import scriptconfig
>>> self = scriptconfig.Config.demo()
>>> oconf = self.to_omegaconf()
argparse(parser=None, special_options=False)[source]

construct or update an argparse.ArgumentParser CLI parser

Parameters:
  • parser (None | argparse.ArgumentParser) – if specified this parser is updated with options from this config.

  • special_options (bool, default=False) – adds special scriptconfig options, namely: –config, –dumps, and –dump.

Returns:

a new or updated argument parser

Return type:

argparse.ArgumentParser

CommandLine

xdoctest -m scriptconfig.config Config.argparse:0
xdoctest -m scriptconfig.config Config.argparse:1

Todo

A good CLI spec for lists might be

# In the case where key ends with and =, assume the list is # given as a comma separated string with optional square brakets at # each end.

–key=[f]

# In the case where key does not end with equals and we know # the value is supposd to be a list, then we consume arguments # until we hit the next one that starts with ‘–’ (which means # that list items cannot start with – but they can contains # commas)

FIXME:

  • In the case where we have an nargs=’+’ action, and we specify the option with an =, and then we give position args after it there is no way to modify behavior of the action to just look at the data in the string without modifying the ArgumentParser itself. The action object has no control over it. For example –foo=bar baz biz will parse as [baz, biz] which is really not what we want. We may be able to overload ArgumentParser to fix this.

Example

>>> # You can now make instances of this class
>>> import scriptconfig
>>> self = scriptconfig.Config.demo()
>>> parser = self.argparse()
>>> parser.print_help()
>>> # xdoctest: +REQUIRES(PY3)
>>> # Python2 argparse does a hard sys.exit instead of raise
>>> ns, extra = parser.parse_known_args()

Example

>>> # You can now make instances of this class
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __description__ = 'my CLI description'
>>>     __default__ = {
>>>         'path1':  scfg.Value(None, position=1, alias='src'),
>>>         'path2':  scfg.Value(None, position=2, alias='dst'),
>>>         'dry':  scfg.Value(False, isflag=True),
>>>         'approx':  scfg.Value(False, isflag=False, alias=['a1', 'a2']),
>>>     }
>>> self = MyConfig()
>>> special_options = True
>>> parser = None
>>> parser = self.argparse(special_options=special_options)
>>> parser.print_help()
>>> self._read_argv(argv=['objection', '42', '--path1=overruled!'])
>>> print('self = {!r}'.format(self))

Example

>>> # Test required option
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __description__ = 'my CLI description'
>>>     __default__ = {
>>>         'path1':  scfg.Value(None, position=1, alias='src'),
>>>         'path2':  scfg.Value(None, position=2, alias='dst'),
>>>         'dry':  scfg.Value(False, isflag=True),
>>>         'important':  scfg.Value(False, required=True),
>>>         'approx':  scfg.Value(False, isflag=False, alias=['a1', 'a2']),
>>>     }
>>> self = MyConfig(data={'important': 1})
>>> special_options = True
>>> parser = None
>>> parser = self.argparse(special_options=special_options)
>>> parser.print_help()
>>> self._read_argv(argv=['objection', '42', '--path1=overruled!', '--important=1'])
>>> print('self = {!r}'.format(self))

Example

>>> # Is it possible to the CLI as a key/val pair or an exist bool flag?
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __default__ = {
>>>         'path1':  scfg.Value(None, position=1, alias='src'),
>>>         'path2':  scfg.Value(None, position=2, alias='dst'),
>>>         'flag':  scfg.Value(None, isflag=True),
>>>     }
>>> self = MyConfig()
>>> special_options = True
>>> parser = None
>>> parser = self.argparse(special_options=special_options)
>>> parser.print_help()
>>> print(self._read_argv(argv=[], strict=True))
>>> # Test that we can specify the flag as a pure flag
>>> print(self._read_argv(argv=['--flag']))
>>> print(self._read_argv(argv=['--no-flag']))
>>> # Test that we can specify the flag with a key/val pair
>>> print(self._read_argv(argv=['--flag', 'TRUE']))
>>> print(self._read_argv(argv=['--flag=1']))
>>> print(self._read_argv(argv=['--flag=0']))
>>> # Test flag and positional
>>> self = MyConfig()
>>> print(self._read_argv(argv=['--flag', 'TRUE', 'SUFFIX']))
>>> self = MyConfig()
>>> print(self._read_argv(argv=['PREFIX', '--flag', 'TRUE']))
>>> self = MyConfig()
>>> print(self._read_argv(argv=['--path2=PREFIX', '--flag', 'TRUE']))

Example

>>> # Test groups
>>> import scriptconfig as scfg
>>> class MyConfig(scfg.Config):
>>>     __description__ = 'my CLI description'
>>>     __default__ = {
>>>         'arg1':  scfg.Value(None, group='a'),
>>>         'arg2':  scfg.Value(None, group='a', alias='a2'),
>>>         'arg3':  scfg.Value(None, group='b'),
>>>         'arg4':  scfg.Value(None, group='b', alias='a4'),
>>>         'arg5':  scfg.Value(None, mutex_group='b', isflag=True),
>>>         'arg6':  scfg.Value(None, mutex_group='b', alias='a6'),
>>>     }
>>> self = MyConfig()
>>> parser = self.argparse()
>>> parser.print_help()
>>> print(self.port_argparse(parser))
>>> import pytest
>>> import argparse
>>> with pytest.raises(SystemExit):
>>>     self._read_argv(argv=['--arg6', '42', '--arg5', '32'])
>>> # self._read_argv(argv=['--arg6', '42', '--arg5']) # Strange, this does not cause an mutex error
>>> self._read_argv(argv=['--arg6', '42'])
>>> self._read_argv(argv=['--arg5'])
>>> self._read_argv(argv=[])
default = {}
normalize()

overloadable function called after each load

class scriptconfig.DataConfig(*args, **kwargs)[source]

Bases: Config

Valid options: []

Parameters:
  • *args – positional arguments for this data config

  • **kwargs – keyword arguments for this data config

classmethod legacy(cmdline=False, data=None, default=None, strict=False)[source]

Calls the original “load” way of creating non-dataclass config objects. This may be refactored in the future.

classmethod parse_args(args=None, namespace=None)[source]

Mimics argparse.ArgumentParser.parse_args

classmethod parse_known_args(args=None, namespace=None)[source]

Mimics argparse.ArgumentParser.parse_known_args

default = {}
classmethod _register_main(func)[source]

Register a function as the main method for this dataconfig CLI

class scriptconfig.Path(value=None, help=None, alias=None)[source]

Bases: Value

Note this is mean to be used only with scriptconfig.Config. It does NOT represent a pathlib object.

cast(value)[source]
class scriptconfig.PathList(value=None, type=None, help=None, choices=None, position=None, isflag=False, nargs=None, alias=None, required=False, short_alias=None, group=None, mutex_group=None, tags=None)[source]

Bases: Value

Can be specified as a list or as a globstr

FIXME:

will fail if there are any commas in the path name

Example

>>> from os.path import join
>>> path = ub.modname_to_modpath('scriptconfig', hide_init=True)
>>> globstr = join(path, '*.py')
>>> # Passing in a globstr is accepted
>>> assert len(PathList(globstr).value) > 0
>>> # Smartcast should separate these
>>> assert len(PathList('/a,/b').value) == 2
>>> # Passing in a list is accepted
>>> assert len(PathList(['/a', '/b']).value) == 2
cast(value=None)[source]
class scriptconfig.Value(value=None, type=None, help=None, choices=None, position=None, isflag=False, nargs=None, alias=None, required=False, short_alias=None, group=None, mutex_group=None, tags=None)[source]

Bases: NiceRepr

You may set any item in the config’s default to an instance of this class. Using this class allows you to declare the desired default value as well as the type that the value should be (Used when parsing sys.argv).

Variables:
  • value (Any) – A float, int, etc…

  • type (type | None) – the “type” of the value. This is usually used if the value specified is not the type that self.value would usually be set to.

  • parsekw (dict) – kwargs for to argparse add_argument

  • position (None | int) – if an integer, then we allow this value to be a positional argument in the argparse CLI. Note, that values with the same position index will cause conflicts. Also note: positions indexes should start from 1.

  • isflag (bool) – if True, args will be parsed as booleans. Default to False.

  • alias (List[str] | None) – other long names (that will be prefixed with ‘–’) that will be accepted by the argparse CLI.

  • short_alias (List[str] | None) – other short names (that will be prefixed with ‘-’) that will be accepted by the argparse CLI.

  • group (str | None) – Impacts display of underlying argparse object by grouping values with the same type together. There is no other impact.

  • mutex_group (str | None) – Indicates that only one of the values in a group should be given on the command line. This has no impact on python usage.

  • tags (Any) – for external program use

CommandLine

xdoctest -m /home/joncrall/code/scriptconfig/scriptconfig/value.py Value
xdoctest -m scriptconfig.value Value

Example

>>> self = Value(None, type=float)
>>> print('self.value = {!r}'.format(self.value))
self.value = None
>>> self.update('3.3')
>>> print('self.value = {!r}'.format(self.value))
self.value = 3.3
update(value)[source]
cast(value)[source]
copy()[source]
_to_value_kw()[source]

Used in port-to-dataconf and port-to-argparse

classmethod _from_action(action, actionid_to_groupkey, actionid_to_mgroupkey, pos_counter)[source]

Used in port_argparse

Example

import argparse from scriptconfig.value import * # NOQA action = argparse._StoreAction(‘foo’, ‘bar’, default=3) value = Value._from_action(action, {}, {}, 0)

action = argparse._CountAction(‘foo’, ‘bar’) value = Value._from_action(action, {}, {}, 0)

scriptconfig.dataconf(cls)[source]

Aims to be similar to the dataclass decorator

Note

It is currently recommended to extend from the DataConfig object instead of decorating with @dataconf. These have slightly different behaviors and the former is more well-tested.

Example

>>> from scriptconfig.dataconfig import *  # NOQA
>>> import scriptconfig as scfg
>>> @dataconf
>>> class ExampleDataConfig2:
>>>     chip_dims = scfg.Value((256, 256), help='chip size')
>>>     time_dim = scfg.Value(3, help='number of time steps')
>>>     channels = scfg.Value('*:(red|green|blue)', help='sensor / channel code')
>>>     time_sampling = scfg.Value('soft2')
>>> cls = ExampleDataConfig2
>>> print(f'cls={cls}')
>>> self = cls()
>>> print(f'self={self}')

Example

>>> from scriptconfig.dataconfig import *  # NOQA
>>> import scriptconfig as scfg
>>> @dataconf
>>> class PathologicalConfig:
>>>     default0 = scfg.Value((256, 256), help='chip size')
>>>     default = scfg.Value((256, 256), help='chip size')
>>>     keys = [1, 2, 3]
>>>     __default__ = {
>>>         'argparse': 3.3,
>>>         'keys': [4, 5],
>>>     }
>>>     default = None
>>>     time_sampling = scfg.Value('soft2')
>>>     def foobar(self):
>>>         ...
>>> self = PathologicalConfig(1, 2, 3)
>>> print(f'self={self}')

# FIXME: xdoctest problem. Need to be able to simulate a module global scope # Example: # >>> # Using inheritance and the decorator lets you pickle the object # >>> from scriptconfig.dataconfig import * # NOQA # >>> import scriptconfig as scfg # >>> @dataconf # >>> class PathologicalConfig2(scfg.DataConfig): # >>> default0 = scfg.Value((256, 256), help=’chip size’) # >>> default2 = scfg.Value((256, 256), help=’chip size’) # >>> #keys = [1, 2, 3] : Too much # >>> __default__3 = { # >>> ‘argparse’: 3.3, # >>> ‘keys2’: [4, 5], # >>> } # >>> default2 = None # >>> time_sampling = scfg.Value(‘soft2’) # >>> config = PathologicalConfig2() # >>> import pickle # >>> serial = pickle.dumps(config) # >>> recon = pickle.loads(serial) # >>> assert ‘locals’ not in str(PathologicalConfig2)

scriptconfig.define(default={}, name=None)[source]

Alternate method for defining a custom Config type

scriptconfig.quick_cli(default, name=None)[source]

Quickly create a CLI

New in 0.5.2

Example

>>> # SCRIPT
>>> import scriptconfig as scfg
>>> default = {
>>>     'fpath': scfg.Path(None),
>>>     'modnames': scfg.Value([]),
>>> }
>>> config = scfg.quick_cli(default)
>>> print('config = {!r}'.format(config))
class scriptconfig.Flag(value=False, **kwargs)[source]

Bases: Value

Exactly the same as a Value except isflag default to True

class scriptconfig.ModalCLI(description='', sub_clis=None, version=None)[source]

Bases: object

Contains multiple scriptconfig.Config items with corresponding main functions.

CommandLine

xdoctest -m scriptconfig.modal ModalCLI

Example

>>> from scriptconfig.modal import *  # NOQA
>>> import scriptconfig as scfg
>>> self = ModalCLI(description='A modal CLI')
>>> #
>>> @self.register
>>> class Command1Config(scfg.Config):
>>>     __command__ = 'command1'
>>>     __default__ = {
>>>         'foo': 'spam'
>>>     }
>>>     @classmethod
>>>     def main(cls, cmdline=1, **kwargs):
>>>         config = cls(cmdline=cmdline, data=kwargs)
>>>         print('config1 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> @self.register
>>> class Command2Config(scfg.DataConfig):
>>>     __command__ = 'command2'
>>>     foo = 'eggs'
>>>     baz = 'biz'
>>>     @classmethod
>>>     def main(cls, cmdline=1, **kwargs):
>>>         config = cls.cli(cmdline=cmdline, data=kwargs)
>>>         print('config2 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> parser = self.argparse()
>>> parser.print_help()
...
A modal CLI
...
commands:
  {command1,command2}  specify a command to run
    command1           argparse CLI generated by scriptconfig...
    command2           argparse CLI generated by scriptconfig...
>>> self.run(argv=['command1'])
config1 = {
    'foo': 'spam',
}
>>> self.run(argv=['command2', '--baz=buz'])
config2 = {
    'foo': 'eggs',
    'baz': 'buz',
}

CommandLine

xdoctest -m scriptconfig.modal ModalCLI:1

Example

>>> # Declarative modal CLI (new in 0.7.9)
>>> import scriptconfig as scfg
>>> class MyModalCLI(scfg.ModalCLI):
>>>     #
>>>     class Command1(scfg.DataConfig):
>>>         __command__ = 'command1'
>>>         foo = scfg.Value('spam', help='spam spam spam spam')
>>>         @classmethod
>>>         def main(cls, cmdline=1, **kwargs):
>>>             config = cls.cli(cmdline=cmdline, data=kwargs)
>>>             print('config1 = {}'.format(ub.urepr(dict(config), nl=1)))
>>>     #
>>>     class Command2(scfg.DataConfig):
>>>         __command__ = 'command2'
>>>         foo = 'eggs'
>>>         baz = 'biz'
>>>         @classmethod
>>>         def main(cls, cmdline=1, **kwargs):
>>>             config = cls.cli(cmdline=cmdline, data=kwargs)
>>>             print('config2 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> MyModalCLI.main(argv=['command1'])
>>> MyModalCLI.main(argv=['command2', '--baz=buz'])

Example

>>> # Declarative modal CLI (new in 0.7.9)
>>> import scriptconfig as scfg
>>> class MyModalCLI(scfg.ModalCLI):
>>>     ...
>>> #
>>> @MyModalCLI.register
>>> class Command1(scfg.DataConfig):
>>>     __command__ = 'command1'
>>>     foo = scfg.Value('spam', help='spam spam spam spam')
>>>     @classmethod
>>>     def main(cls, cmdline=1, **kwargs):
>>>         config = cls.cli(cmdline=cmdline, data=kwargs)
>>>         print('config1 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> @MyModalCLI.register
>>> class Command2(scfg.DataConfig):
>>>     __command__ = 'command2'
>>>     foo = 'eggs'
>>>     baz = 'biz'
>>>     @classmethod
>>>     def main(cls, cmdline=1, **kwargs):
>>>         config = cls.cli(cmdline=cmdline, data=kwargs)
>>>         print('config2 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> MyModalCLI.main(argv=['command1'])
>>> MyModalCLI.main(argv=['command2', '--baz=buz'])
property sub_clis
classmethod register(cli_cls)[source]
Parameters:

cli_cli (scriptconfig.Config) – A CLI-aware config object to register as a sub CLI

_build_subcmd_infos()[source]
_parserkw()[source]

Generate the kwargs for making a new argparse.ArgumentParser

argparse(parser=None, special_options=Ellipsis)[source]

Builds a new argparse object for this ModalCLI or extends an existing one with it.

build_parser(parser=None, special_options=Ellipsis)

Builds a new argparse object for this ModalCLI or extends an existing one with it.

classmethod main(argv=None, strict=True, autocomplete='auto')[source]

Execute the modal CLI as the main script

classmethod run(argv=None, strict=True, autocomplete='auto')

Execute the modal CLI as the main script