"""
Write simple configs and update from CLI, kwargs, and/or json.
The ``scriptconfig`` provides a simple way to make configurable scripts using a
combination of config files, command line arguments, and simple Python keyword
arguments. A script config object is defined by creating a subclass of
``Config`` with a ``default`` dict class attribute. An instance of a custom
``Config`` object will behave similar a dictionary, but with a few
conveniences.
Note:
* This class implements the old-style legacy Config class, new applications
should favor using DataConfig instead, which has simpler boilerplate.
To get started lets consider some example usage:
Example:
>>> import scriptconfig as scfg
>>> # In its simplest incarnation, the config class specifies default values.
>>> # For each configuration parameter.
>>> class ExampleConfig(scfg.Config):
>>> __default__ = {
>>> 'num': 1,
>>> 'mode': 'bar',
>>> 'ignore': ['baz', 'biz'],
>>> }
>>> # Creating an instance, starts using the defaults
>>> config = ExampleConfig()
>>> # Typically you will want to update default from a dict or file. By
>>> # specifying cmdline=True you denote that it is ok for the contents of
>>> # `sys.argv` to override config values. Here we pass a dict to `load`.
>>> kwargs = {'num': 2}
>>> config.load(kwargs, cmdline=False)
>>> assert config['num'] == 2
>>> # The `load` method can also be passed a json/yaml file/path.
>>> import tempfile
>>> config_fpath = tempfile.mktemp()
>>> open(config_fpath, 'w').write('{"num": 3}')
>>> config.load(config_fpath, cmdline=False)
>>> assert config['num'] == 3
>>> # It is possible to load only from CLI by setting cmdline=True
>>> # or by setting it to a custom sys.argv
>>> config.load(cmdline=['--num=4', '--mode' ,'fiz'])
>>> assert config['num'] == 4
>>> assert config['mode'] == 'fiz'
>>> # You can also just use the command line string itself
>>> config.load(cmdline='--num=4 --mode fiz')
>>> assert config['num'] == 4
>>> assert config['mode'] == 'fiz'
>>> # Note that using `config.load(cmdline=True)` will just use the
>>> # contents of sys.argv
Ignore:
>>> class ExampleConfig(scfg.Config):
>>> __default__ = {
>>> 'num': 1,
>>> 'mode': 'bar',
>>> 'mode2': scfg.Value('bar', str),
>>> 'ignore': ['baz', 'biz'],
>>> }
>>> config = ExampleConfig()
>>> # smartcast can handle lists as long as there are no spaces
>>> config.load(cmdline=['--ignore=spam,eggs'])
>>> assert config['ignore'] == ['spam', 'eggs']
>>> # Note that the Value type can influence how data is parsed
>>> config.load(cmdline=['--mode=spam,eggs', '--mode2=spam,eggs'])
>>> # FIXME: We need make parsing lists a bit more intuitive
>>> class ExampleConfig(scfg.Config):
>>> __default__ = {
>>> 'item1': [],
>>> 'item2': scfg.Value([], list),
>>> 'item3': scfg.Value([]),
>>> }
>>> config = ExampleConfig()
>>> # IDEALLY BOTH CASES SHOULD WORK
>>> config.load(cmdline=['--item1', 'spam', 'eggs', '--item2', 'spam', 'eggs', '--item3', 'spam', 'eggs'])
>>> print(ub.urepr(config, nl=1))
>>> config.load(cmdline=['--item1=spam,eggs', '--item2=spam,eggs', '--item3=spam,eggs'])
>>> print(ub.urepr(config, nl=1))
TODO:
- [ ] Handle Nested Configs?
- [ ] Integrate with Hyrda
- [x] Dataclass support - See DataConfig
"""
import ubelt as ub
import itertools as it
from collections import OrderedDict
from scriptconfig import _ubelt_repr_extension
from scriptconfig import smartcast
from scriptconfig.dict_like import DictLike
from scriptconfig.file_like import FileLike
from scriptconfig.value import Value
# from scriptconfig.util.util_class import class_or_instancemethod
__all__ = ['Config', 'define']
__docstubs__ = """
from typing import Any
KT = Any
omegaconf: Any
OmegaConf: object
"""
# def _is_autoreload_enabled():
# """
# Detect if IPython has autoreloaded this module
# https://stackoverflow.com/questions/63469147/programmatically-check-for-and-disable-ipython-autoreload-extension
# """
# try:
# __IPYTHON__
# except NameError:
# return False
# else:
# from IPython import get_ipython
# ipy = get_ipython()
# return ipy.magics_manager.magics['line']['autoreload'].__self__._reloader.enabled
def scfg_isinstance(item, cls):
"""
use instead isinstance for scfg types when reloading
Args:
item (object): instance to check
cls (type): class to check against
Returns:
bool
"""
# Note: it is safe to simply use isinstance(item, cls) when
# not reloading
if hasattr(item, '__scfg_class__') and hasattr(cls, '__scfg_class__'):
return item.__scfg_class__ == cls.__scfg_class__
else:
return isinstance(item, cls)
[docs]
def define(default={}, name=None):
"""
Alternate method for defining a custom Config type
"""
import uuid
if name is None:
hashid = str(uuid.uuid4()).replace('-', '_')
name = 'Config_{}'.format(hashid)
from textwrap import dedent
vals = {}
code = dedent(
'''
import scriptconfig as scfg
class {name}(scfg.Config):
pass
'''.strip('\n').format(name=name))
exec(code, vals)
cls = vals[name]
return cls
class MetaConfig(type):
"""
A metaclass for Config to help make usage between Config and DataConfig
consistent.
Ensures that class attributes are mirrored:
* __default__ mirrors default
* __post_init__ mirrors normalize
"""
@staticmethod
def __new__(mcls, name, bases, namespace, *args, **kwargs):
# print(f'MetaConfig.__new__ called: {mcls=} {name=} {bases=} {namespace=} {args=} {kwargs=}')
if 'default' in namespace and '__default__' not in namespace:
# Ensure the user updates to the newer "__default__" paradigm
this_default = namespace['__default__'] = namespace['default']
ub.schedule_deprecation(
'scriptconfig', 'default', f'class attribute of {name}',
migration='Use __default__ instead',
deprecate='0.7.6', error='0.8.0', remove='0.9.0',
)
HANDLE_INHERITENCE = 1
if HANDLE_INHERITENCE:
# Handle inheritence, add in defaults from base classes
# Not sure this is exactly correct. Experimental.
this_default = namespace.get('__default__', {})
if this_default is None:
this_default = {}
this_default = ub.udict(this_default)
inheritence_default = {}
for base in bases:
if hasattr(base, '__default__'):
inheritence_default.update(base.__default__)
# unseen = base.__default__ - this_default
# this_default.update(unseen)
inheritence_default.update(this_default)
this_default = inheritence_default
namespace['__default__'] = namespace['default'] = this_default
if '__default__' in namespace and 'default' not in namespace:
# Backport to the older non-dunder __default__
namespace['default'] = namespace['__default__']
if 'normalize' in namespace and '__post_init__' not in namespace:
# Ensure the newer __post_init__ is specified
namespace['__post_init__'] = namespace['normalize']
ub.schedule_deprecation(
'scriptconfig', 'normalize', f'class attribute of {name}',
migration='Use __post_init__ instead',
deprecate='0.7.6', error='0.8.0', remove='0.9.0',
)
if '__post_init__' in namespace and 'normalize' not in namespace:
# Backport to the older non-dunder normalize
namespace['normalize'] = namespace['__post_init__']
# print('FINAL namespace = {}'.format(ub.urepr(namespace, nl=2)))
cls = super().__new__(mcls, name, bases, namespace, *args, **kwargs)
return cls
[docs]
class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
"""
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 :class:`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 :class:`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.
Attributes:
_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'))
"""
__scfg_class__ = 'Config'
__default__ = {}
def __init__(self, data=None, default=None, cmdline=False,
_dont_call_post_init=False):
"""
Args:
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 :func:`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..
"""
# The _data attribute holds
self._data = None
self._default = OrderedDict()
cls_default = getattr(self, '__default__', getattr(self, 'default', None))
if cls_default:
# allow for class attributes to specify the default
self._default.update(cls_default)
self._alias_map = None
self.load(data, cmdline=cmdline, default=default,
_dont_call_post_init=_dont_call_post_init)
[docs]
@classmethod
def cli(cls, data=None, default=None, argv=None, strict=True,
cmdline=True, autocomplete='auto'):
"""
Create a commandline aware config instance.
Calls the original "load" way of creating non-dataclass config objects.
This may be refactored in the future.
Args:
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.
"""
if cmdline and argv is not None:
cmdline = argv
if default is None:
default = {}
# Note: hack to avoid calling __post_init__ twice
# We may want to refactor this to be a bit nicer.
# Might require a major version bump and breaking of backwards compat.
# avoid this. The thing that makes this difficult is the DataConfig
# init method taking in keyword args corresponding to the config which
# prevents adding clean options for control flow.
self = cls(_dont_call_post_init=True)
self.load(data, cmdline=cmdline, default=default, strict=strict,
autocomplete=autocomplete)
return self
[docs]
@classmethod
def demo(cls):
"""
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))
"""
import scriptconfig as scfg
class DemoConfig(scfg.Config):
"""
This was generated by scriptconfig.Config.demo
"""
__default__ = {
'option1': scfg.Value('bar', help='an option'),
'option2': scfg.Value((1, 2, 3), tuple, help='another option'),
'option3': None,
'option4': 'foo',
'discrete': scfg.Value(None, choices=['a', 'b', 'c']),
'apath': scfg.Path(help='a path'),
}
self = DemoConfig()
return self
def __json__(self):
"""
Creates a JSON serializable representation of this config object.
Raises:
TypeError: if any non-builtin python objects without a __json__
method are encountered.
Returns:
dict
Example:
>>> self = Config.demo()
>>> self.__json__()
>>> self['option1'] = {1, 2, 3}
>>> self['option2'] = {(1, 2): 'fds'}
>>> self.__json__()
"""
try:
import numpy
except ImportError:
numpy = None
data = self.asdict()
BUILTIN_SCALAR_TYPES = (str, int, float, complex)
BUILTIN_VECTOR_TYPES = (set, frozenset, list, tuple)
# The walker method should be more efficient.
walker = ub.IndexableWalker(data, list_cls=BUILTIN_VECTOR_TYPES)
for path, item in walker:
if item is None or isinstance(item, BUILTIN_SCALAR_TYPES):
...
elif isinstance(item, list):
...
elif isinstance(item, (set, tuple)):
walker[path] = list(item)
elif numpy is not None and isinstance(item, numpy.ndarray):
walker[path] = item.tolist()
elif isinstance(item, OrderedDict):
...
elif isinstance(item, dict):
walker[path] = OrderedDict(sorted(item.items()))
else:
if hasattr(item, '__json__'):
return item.__json__()
else:
raise TypeError(
'Unknown JSON serialization for type {!r}'.format(type(item)))
return data
def __nice__(self):
return str(self.asdict())
[docs]
def getitem(self, key):
"""
Dictionary-like method to get the value of a key.
Args:
key (str): the key
Returns:
Any : the associated value
"""
try:
value = self._data[key]
except KeyError:
# Attempt alias
key = self._normalize_alias_key(key)
value = self._data[key]
if scfg_isinstance(value, Value):
value = value.value
return value
[docs]
def setitem(self, key, value):
"""
Dictionary-like method to set the value of a key.
Args:
key (str): the key
value (Any): the new value
"""
if key not in self._data:
key = self._normalize_alias_key(key)
if key not in self._data:
raise Exception('Cannot add keys to ScriptConfig objects')
if scfg_isinstance(value, Value):
# If the new item is a Value object simply overwrite the old one
self._data[key] = value
else:
template = self.__default__[key]
if scfg_isinstance(template, Value):
# If the new value is raw data, and we have a underlying Value
# object update it.
self._data[key] = template.cast(value)
else:
# If we don't have an underlying Value object simply set the
# raw data.
self._data[key] = value
[docs]
def delitem(self, key):
raise Exception('cannot delete items from a config')
[docs]
def keys(self):
"""
Dictionary-like keys method
Yields:
str:
"""
return self._data.keys()
[docs]
def update_defaults(self, default):
"""
Update the instance-level default values
Args:
default (dict): new defaults
"""
import copy
default = self._normalize_alias_dict(default)
# The user might pass raw values in which case we should keep the
# metadata from the exsting wrapped Values and just update the .value
# attribute.
for k, v in default.items():
old_default = self._default[k]
if scfg_isinstance(old_default, Value) and not scfg_isinstance(v, Value):
new_default = copy.deepcopy(old_default)
new_default.value = v
default[k] = new_default
self._default.update(default)
self._alias_map = None
[docs]
def load(self, data=None, cmdline=False, mode=None, default=None,
strict=False, autocomplete=False, _dont_call_post_init=False):
"""
Updates the configuration from a given data source.
Any option can be overwritten via the command line if ``cmdline`` is
truthy.
Args:
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 :func:`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
"""
if default:
self.update_defaults(default)
# Maybe this shouldn't be a deep copy?
import copy
_default = copy.deepcopy(self._default)
if mode is None:
if isinstance(data, str):
if data.lower().endswith('.json'):
mode = 'json'
if mode is None:
# Default to yaml
mode = 'yaml'
if data is None:
user_config = {}
elif isinstance(data, str) or hasattr(data, 'readable'):
import yaml
with FileLike(data, 'r') as file:
user_config = yaml.load(file, Loader=yaml.SafeLoader)
user_config.pop('__heredoc__', None) # ignore special heredoc key
elif isinstance(data, dict):
user_config = data
elif scfg_isinstance(data, Config):
user_config = data.asdict()
else:
raise TypeError(
'Expected path or dict, but got {}'.format(type(data)))
# check for unknown values
indirect_keys = set(user_config) - set(_default)
if indirect_keys:
# Check if unknown keys are aliases
unknown_keys = []
_alias_map = self._build_alias_map()
for a in indirect_keys:
if a in _alias_map:
k = _alias_map[a]
user_config[k] = user_config.pop(a)
else:
unknown_keys.append(a)
if unknown_keys:
raise KeyError('Unknown data options {}'.format(unknown_keys))
self._data = _default.copy()
self.update(user_config)
if isinstance(cmdline, str):
# allow specification using the actual command line arg string
import shlex
import os
cmdline = shlex.split(os.path.expandvars(cmdline))
if cmdline or ub.iterable(cmdline):
# TODO: if user_config is specified, then we should probably not
# override any values in user_config with the defaults? The CLI
# should override them IF they exist on in sys.argv, but not if
# they don't?
read_argv_kwargs = {
'special_options': True,
'strict': strict,
'autocomplete': autocomplete,
'argv': None,
}
if isinstance(cmdline, dict):
read_argv_kwargs.update(cmdline)
elif ub.iterable(cmdline) or isinstance(cmdline, str):
read_argv_kwargs['argv'] = cmdline
self._read_argv(**read_argv_kwargs)
if 1:
# Check that all required variables are not the same as defaults
# Probably a way to make this check nicer
for k, v in self._default.items():
if scfg_isinstance(v, Value):
if v.required:
if self[k] == v.value:
raise Exception('Required variable {!r} still has default value'.format(k))
if not _dont_call_post_init:
self.__post_init__()
return self
[docs]
def _normalize_alias_key(self, key):
"""
normalizes a single aliased key
"""
if getattr(self, '_alias_map', None) is None:
self._alias_map = self._build_alias_map()
return self._alias_map[key]
[docs]
def _normalize_alias_dict(self, data):
"""
Args:
data (dict): dictionary with keys that could be aliases
Returns:
dict: keys are normalized to be primary keys.
"""
if getattr(self, '_alias_map', None) is None:
self._alias_map = self._build_alias_map()
norm = {self._alias_map.get(k, k): v for k, v in data.items()}
return norm
[docs]
def _build_alias_map(self):
_alias_map = {}
for k, v in self._default.items():
alias = getattr(v, 'alias', None)
if alias:
if not ub.iterable(alias):
alias = [alias]
for a in alias:
_alias_map[a] = k
return _alias_map
[docs]
def _read_argv(self, argv=None, special_options=True, strict=False, autocomplete=False):
"""
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()
Ignore:
>>> # Weird cases
>>> self = MyConfig()
>>> self._read_argv(argv='--src=[p4,p5,p6!] f of')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='--src=p4,')
>>> print('self = {}'.format(self))
>>> self = MyConfig()
>>> self._read_argv(argv='a b --src p4 p5 p6!')
>>> 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!')
>>> print('self = {}'.format(self))
"""
if isinstance(argv, str):
import shlex
argv = shlex.split(argv)
# TODO: warn about any unused flags
parser = self.argparse(special_options=special_options)
if autocomplete:
try:
import argcomplete as argcomplete_mod
except ImportError:
if autocomplete != 'auto':
raise
else:
argcomplete_mod.autocomplete(parser)
if strict:
ns = parser.parse_args(argv).__dict__
else:
ns = parser.parse_known_args(argv)[0].__dict__
special_ns_keys = ['config', 'dump', 'dumps']
if special_options:
special_ns = {k: ns.pop(k, None) for k in special_ns_keys}
else:
special_ns = {}
# We might remove code under this if using action casting proves to be
# stable.
RELY_ON_ACTION_SMARTCAST = True
# First load argparse defaults in first
_not_given = set(ns.keys()) - parser._explicitly_given
# print('_not_given = {!r}'.format(_not_given))
# print('parser._explicitly_given = {!r}'.format(parser._explicitly_given))
for key in _not_given:
value = ns[key]
# NOTE: this implementation is messy and needs refactor.
# Currently the .__default__ .default, ._default, and ._data
# attributes can all be Value objects, but this gets messy when the
# "default" constructor argument is used. We should refactor so
# _data and _default only store the raw current values,
# post-casting.
if key not in self.__default__:
# probably an alias
continue
if not RELY_ON_ACTION_SMARTCAST:
# Old way that we did smartcast. Hopefully the action class
# takes care of this.
template = self.__default__[key]
# print('template = {!r}'.format(template))
if not isinstance(template, Value):
# smartcast non-valued params from commandline
value = smartcast.smartcast(value)
else:
value = template.cast(value)
# if value is not None:
self[key] = value
# Then load config file defaults
if special_options:
config_fpath = special_ns['config']
if config_fpath is not None:
self.load(config_fpath, cmdline=False,
_dont_call_post_init=True)
# Finally load explicit CLI values
for key in parser._explicitly_given:
if key not in special_ns:
value = ns[key]
if not RELY_ON_ACTION_SMARTCAST:
# Old way that we did smartcast. Hopefully the action class
# takes care of this.
template = self.__default__[key]
# print('value = {!r}'.format(value))
# print('template = {!r}'.format(template))
if not isinstance(template, Value):
# smartcast non-valued params from commandline
value = smartcast.smartcast(value)
# if value is not None:
self[key] = value
# We dont want this here right?
# self.__post_init__()
if special_options:
import sys
dump_fpath = special_ns['dump']
do_dumps = special_ns['dumps']
if dump_fpath or do_dumps:
if dump_fpath:
# Infer config format from the extension
if dump_fpath.lower().endswith('.json'):
mode = 'json'
elif dump_fpath.lower().endswith('.yaml'):
mode = 'yaml'
else:
mode = 'yaml'
text = self.dumps(mode=mode)
with open(dump_fpath, 'w') as file:
file.write(text)
if do_dumps:
# Always use yaml to dump to stdout
text = self.dumps(mode='yaml')
print(text)
sys.exit(1)
return self
def __post_init__(self):
""" overloadable function called after each load """
...
[docs]
def dump(self, stream=None, mode=None):
"""
Write configuration file to a file or stream
Args:
stream (FileLike | None): the stream to write to
mode (str | None): can be 'yaml' or 'json' (defaults to 'yaml')
"""
if mode is None:
mode = 'yaml'
if mode == 'yaml':
import yaml
def order_rep(dumper, data):
return dumper.represent_mapping('tag:yaml.org,2002:map', data.items(), flow_style=False)
yaml.add_representer(OrderedDict, order_rep)
return yaml.safe_dump(dict(self.items()), stream)
elif mode == 'json':
import json
json_text = json.dumps(OrderedDict(self.items()), indent=4)
return json_text
else:
raise KeyError(mode)
[docs]
def dumps(self, mode=None):
"""
Write the configuration to a text object and return it
Args:
mode (str | None): can be 'yaml' or 'json' (defaults to 'yaml')
Returns:
str - the configuration as a string
"""
return self.dump(mode=mode)
def __getattr__(self, key):
# Handle aliasing of old "default" and new "__default__"
if key == 'default' and hasattr(self, '__default__'):
return self.__default__
elif key == '__default__' and hasattr(self, 'default'):
return self.default
raise AttributeError(key)
@property
def _description(self):
if hasattr(self, 'description'):
ub.schedule_deprecation(
'scriptconfig', 'description', 'attribute of Config classes',
migration='Use __description__ or the docstring instead',
deprecate='0.7.11', error='0.8.0', remove='0.9.0')
description = getattr(self, '__description__',
getattr(self, 'description', None))
if description is None:
description = self.__class__.__doc__
if description is None:
import scriptconfig
description = f'argparse CLI generated by scriptconfig {scriptconfig.__version__}'
if description is not None:
description = ub.codeblock(description)
return description
@property
def _epilog(self):
if hasattr(self, 'epilog'):
ub.schedule_deprecation(
'scriptconfig', 'epilog', 'attribute of Config classes',
migration='Use __epilog__ instead',
deprecate='0.7.11', error='0.8.0', remove='0.9.0')
epilog = getattr(self, '__epilog__', getattr(self, 'epilog', None))
if epilog is not None:
epilog = ub.codeblock(epilog)
return epilog
@property
def _prog(self):
if hasattr(self, 'prog'):
ub.schedule_deprecation(
'scriptconfig', 'prog', 'attribute of Config classes',
migration='Use __prog__ instead',
deprecate='0.7.11', error='0.8.0', remove='0.9.0')
prog = getattr(self, '__prog__', getattr(self, 'prog', None))
if prog is None:
prog = self.__class__.__name__
return prog
[docs]
def _parserkw(self):
"""
Generate the kwargs for making a new argparse.ArgumentParser
"""
from scriptconfig import argparse_ext
parserkw = dict(
prog=self._prog,
description=self._description,
epilog=self._epilog,
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
# formatter_class=argparse.RawDescriptionHelpFormatter,
formatter_class=argparse_ext.RawDescriptionDefaultsHelpFormatter,
# exit_on_error=False,
)
if hasattr(self, '__allow_abbrev__'):
parserkw['allow_abbrev'] = self.__allow_abbrev__
return parserkw
[docs]
def port_to_dataconf(self):
"""
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())
"""
entries = []
for key, value in self.__default__.items():
if not scfg_isinstance(value, Value):
value_kw = Value(value)._to_value_kw()
else:
value_kw = value._to_value_kw()
entries.append((key, value_kw))
description = self._description
name = self.__class__.__name__
style = 'dataconf'
text = self._write_code(entries, name, style, description)
return text
[docs]
@classmethod
def _write_code(self, entries, name='MyConfig', style='dataconf', description=None):
if style == 'dataconf':
indent = ' ' * 4
else:
indent = ' ' * 8
if style == 'orig':
recon_str = [
'import ubelt as ub',
'import scriptconfig as scfg',
'',
'class ' + name + '(scfg.Config):',
' """',
ub.indent(description or ''),
' """',
' __default__ = {',
]
elif style == 'dataconf':
recon_str = [
'import ubelt as ub',
'import scriptconfig as scfg',
'',
'class ' + name + '(scfg.DataConfig):',
' """',
ub.indent(description or ''),
' """',
]
else:
raise KeyError(style)
for (key, value_kw) in entries:
_value_kw = value_kw.copy()
default = _value_kw.pop('default')
value_args = [
repr(default),
]
value_args.extend(['{}={}'.format(k, repr(v)) for k, v in _value_kw.items() if v is not None])
val_body = ', '.join(value_args)
if style == 'orig':
recon_str.append("{}'{}': scfg.Value({}),".format(indent, key, val_body))
elif style == 'dataconf':
recon_str.append("{}{} = scfg.Value({})".format(indent, key, val_body))
else:
raise KeyError(style)
if style == 'orig':
recon_str.append(' }')
elif style == 'dataconf':
...
else:
raise KeyError(style)
text = '\n'.join(recon_str)
if 0:
try:
import black
text = black.format_str(
text, mode=black.Mode(string_normalization=True)
)
except Exception:
pass
return text
[docs]
@classmethod
def port_click(cls, click_main, name='MyConfig', style='dataconf'):
"""
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):
...
"""
raise NotImplementedError('todo: figure out how to do this')
import click
ctx = click.Context(click.Command(''))
# parser = click_main.make_parser(ctx)
# print(f'parser={parser}') # not an argparse object
info_dict = click_main.to_info_dict(ctx) # NOQA
[docs]
@classmethod
def port_argparse(cls, parser, name='MyConfig', style='dataconf'):
"""
Generate the corresponding scriptconfig code from an existing argparse
instance.
Args:
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:
str :
code to create a scriptconfig object that should work similarly
to the existing argparse object.
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)))
"""
# This logic should be able to be used statically or dynamically
# to transition argparse to ScriptConfig code.
pos_counter = it.count(1)
# Determine if the parser has groups / mutex groups. Build mappings so
# we can lookup which action is associated with which group later.
group_counter = it.count(1)
mgroup_counter = it.count(1)
annon_groupid_to_key = {}
annon_mgroupid_to_key = {}
default_groups = {'positional arguments', 'options', 'required'}
actionid_to_groupkey = {}
actionid_to_mgroupkey = {}
# Build group lookups table
for group in parser._action_groups:
if group.title not in default_groups:
if group.title is not None:
group_key = group.title
else:
group_id = id(group)
if group_id not in annon_groupid_to_key:
annon_groupid_to_key[group_id] = next(group_counter)
group_key = annon_groupid_to_key[group_id]
for action in group._group_actions:
action_id = id(action)
actionid_to_groupkey[action_id] = group_key
# Build mutex group lookups table
for mutex_group in parser._mutually_exclusive_groups:
mgroup_id = id(mutex_group)
if mgroup_id not in annon_mgroupid_to_key:
annon_mgroupid_to_key[mgroup_id] = next(mgroup_counter)
mgroup_key = annon_mgroupid_to_key[mgroup_id]
for action in mutex_group._group_actions:
action_id = id(action)
actionid_to_mgroupkey[action_id] = mgroup_key
# Iterate over all of the actions and build the appropriate value to be
# placed in the scriptconfig class.
entries = []
for action in parser._actions:
key = action.dest
if key == 'help':
# scriptconfig takes care of help for us
continue
value = Value._from_action(
action, actionid_to_groupkey, actionid_to_mgroupkey, pos_counter)
value_kw = value._to_value_kw()
entries.append((key, value_kw))
description = parser.description
text = cls._write_code(entries, name, style, description)
return text
[docs]
def port_to_argparse(self):
"""
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:
:meth:`Config.argparse` - creates a real argparse object
Returns:
str: code to construct a similar argparse object
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)))
"""
parserkw = self._parserkw()
to_pop = {k for k, v in parserkw.items() if v is None}
parserkw = ub.udict(parserkw) - to_pop
parserkw.pop('formatter_class', None)
constructor_body = ub.indent(ub.urepr(parserkw, explicit=True, nobr=1))
lines = []
lines.append(ub.codeblock(
'''
import argparse
parser = argparse.ArgumentParser(
{constructor_body}
formatter_class=argparse.RawDescriptionHelpFormatter,
)
''').format(
constructor_body=constructor_body,
))
from scriptconfig import value as value_mod
for key, _value in self._data.items():
if isinstance(_value, value_mod.Value):
value = _value.value
else:
value = _value
_value = self._default[key]
invocations = value_mod._value_add_argument_kw(value, _value, self, key)
for arg_type, t in invocations.items():
meth, args, kwargs = t
if not isinstance(kwargs.get('action'), str):
kwargs.pop('action')
if kwargs.get('type', None) is not None:
kwargs['type'] = value_mod.CodeRepr(kwargs['type'].__name__)
to_pop = {k for k, v in kwargs.items() if v is None}
kwargs = ub.udict(kwargs) - to_pop
args_body = ub.urepr(args, explicit=1, nobr=1, trailsep=0).strip().strip(',')
kwargs_body = ub.urepr(kwargs, explicit=1, nobr=1, trailsep=0, nl=0).strip(',')
if args_body and kwargs_body:
args_body += ', '
lines.append(f'parser.{meth}({args_body}{kwargs_body})')
text = '\n'.join(lines)
return text
# @classmethod
# def _construct_config_text(cls):
# ...
@property
def namespace(self):
"""
Access a namespace like object for compatibility with argparse
Returns:
argparse.Namespace
"""
from argparse import Namespace
return Namespace(**dict(self))
[docs]
def to_omegaconf(self):
"""
Creates an omegaconfig version of this.
Returns:
omegaconf.OmegaConf:
Example:
>>> # xdoctest: +REQUIRES(module:omegaconf)
>>> import scriptconfig
>>> self = scriptconfig.Config.demo()
>>> oconf = self.to_omegaconf()
"""
from omegaconf import OmegaConf
oconf = OmegaConf.create(self.to_dict())
return oconf
[docs]
def argparse(self, parser=None, special_options=False):
"""
construct or update an argparse.ArgumentParser CLI parser
Args:
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:
argparse.ArgumentParser : a new or updated argument parser
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))
Ignore:
>>> self._read_argv(argv=['hi','--path1=foobar'])
>>> self._read_argv(argv=['hi', 'hello', '--path1=foobar'])
>>> self._read_argv(argv=['hi', 'hello', '--path1=foobar', '--help'])
>>> self._read_argv(argv=['--path1=foobar', '--path1=baz'])
>>> 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=[])
"""
from scriptconfig import argparse_ext
if parser is None:
parserkw = self._parserkw()
# parser = argparse.ArgumentParser(**parserkw)
parser = argparse_ext.CompatArgumentParser(**parserkw)
# Use custom action used to mark which values were explicitly set on
# the commandline
parser._explicitly_given = set()
# IRC: this ensures each key has a real Value class
# This is messy and needs to be rethought
_metadata = {
key: self._data[key]
for key, value in self._default.items()
if isinstance(self._data[key], Value)
} # :type: Dict[str, Value]
for k, v in self._default.items():
# If the _data did not have value information but the _default
# does, use that. This is very ugly.
if k not in _metadata:
if isinstance(v, Value):
_metadata[k] = v.copy().update(self._data[k])
_positions = {k: v.position for k, v in _metadata.items()
if v.position is not None}
if _positions:
if ub.find_duplicates(_positions.values()):
# TODO: make this a warning in 3.7+ and ensure there is a good
# API for just indicating that a value is supposed to be
# positional, and using its order in the dictionary as that
# position. Need to account for inheritence though.
raise Exception('two values have the same position')
_keyorder = ub.oset(ub.argsort(_positions))
_keyorder |= (ub.oset(self._default) - _keyorder)
else:
_keyorder = list(self._default.keys())
FUZZY_HYPHENS = getattr(self, '__fuzzy_hyphens__', 1)
# Need to clean this up, metadata probably isn't necessary.
for key, value in self._data.items():
if key in _metadata:
# Use the metadata in the Value class to enhance argparse
_value = _metadata[key]
else:
# _value = value if scfg_isinstance(value, Value) else None
if scfg_isinstance(value, Value):
raise AssertionError('Did not expect {value=} to be a Value')
else:
# In this case the user did not wrap the default with a
# Value, so we can only infer so much about it, but we can
# make some educated guesses.
_autokw = {
'help': '',
}
if isinstance(value, bool) or isinstance(value, int) and value in {0, 1}:
# In this case they probably wanted a boolean flag
# In any case it restrict functionality to set isflag=1
_autokw['isflag'] = True
_value = Value(value, **_autokw)
from scriptconfig import value as value_mod
value_mod._value_add_argument_to_parser(
value, _value, self, parser, key, fuzzy_hyphens=FUZZY_HYPHENS)
if special_options:
special_group = parser.add_argument_group(
'scriptconfig options')
special_group.add_argument('--config', default=None, help=ub.codeblock(
'''
special scriptconfig option that accepts the path to a on-disk
configuration file, and loads that into this {!r} object.
''').format(self.__class__.__name__))
special_group.add_argument('--dump', default=None, help=ub.codeblock(
'''
If specified, dump this config to disk.
''').format(self.__class__.__name__))
special_group.add_argument(
'--dumps', action=argparse_ext.BooleanFlagOrKeyValAction,
help=ub.codeblock(
'''
If specified, dump this config stdout
''').format(self.__class__.__name__))
return parser
__notes__ = """
export _ARC_DEBUG=1
pip install argcomplete
activate-global-python-argcomplete --dest=$HOME/.bash_completion.d --user
eval "$(register-python-argcomplete xdev)"
complete -r xdev
"""
_ubelt_repr_extension._register_ubelt_repr_extensions()