# Copyright (C) 2018 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from typing import Union, List, Optional from argparse import ArgumentParser, Namespace, SUPPRESS, _HelpAction, HelpFormatter try: from tiramisu import Config from tiramisu.error import PropertiesOptionError except ImportError: Config = None from tiramisu_json_api.error import PropertiesOptionError try: from tiramisu_json_api import Config as ConfigJson if Config is None: Config = ConfigJson except ImportError: ConfigJson = Config class TiramisuNamespace(Namespace): def _populate(self): #self._config.property.read_only() for tiramisu_key, tiramisu_value in self._config.value.dict().items(): option = self._config.option(tiramisu_key) if not option.option.issymlinkoption(): if tiramisu_value == [] and option.option.ismulti() and option.owner.isdefault(): tiramisu_value = None super().__setattr__(tiramisu_key, tiramisu_value) #self._config.property.read_write() def __init__(self, config): self._config = config super().__init__() def __setattr__(self, key, value): if key == '_config': super().__setattr__(key, value) return # self._config.property.read_write() option = self._config.option(key) if option.option.ismulti() and value is not None and not isinstance(value, list): value = [value] option.value.set(value) def __getattribute__(self, key): if key == '__dict__' and hasattr(self, '_config'): self._populate() return super().__getattribute__(key) class TiramisuHelpFormatter(HelpFormatter): def _get_default_metavar_for_optional(self, action): ret = super()._get_default_metavar_for_optional(action) if '.' in ret: ret = ret.split('.', 1)[1] return ret class _TiramisuHelpAction(_HelpAction): needs = False def __call__(self, *args, **kwargs): _TiramisuHelpAction.needs = True def display(self, parser): _HelpAction.__call__(self, parser, None, None) class TiramisuCmdlineParser(ArgumentParser): def __init__(self, config: Union[Config, ConfigJson], *args, fullpath: bool=True, _forhelp: bool=False, **kwargs): self.fullpath = fullpath self.config = config kwargs['formatter_class'] = TiramisuHelpFormatter super().__init__(*args, **kwargs) self.register('action', 'help', _TiramisuHelpAction) self._config_to_argparser(_forhelp, self.config.option) def _pop_action_class(self, kwargs, default=None): ret = super()._pop_action_class(kwargs, default) if kwargs.get('action') != 'help' and kwargs.get('dest') != 'help': return ret return _TiramisuHelpAction def _match_arguments_partial(self, actions, arg_string_pattern): # used only when check first proposal for first value # we have to remove all actions with propertieserror # so only first settable option will be returned actions_pop = [] for idx, action in enumerate(actions): if self.config.option(action.dest).property.get(only_raises=True): actions_pop.append(idx) else: break for idx in actions_pop: actions.pop(0) return super()._match_arguments_partial(actions, arg_string_pattern) def _parse_known_args(self, args=None, namespace=None): namespace_, args_ = super()._parse_known_args(args, namespace) if args != args_ and args_ and args_[0].startswith(self.prefix_chars): # option that was disabled are no more disable # so create a new parser new_parser = TiramisuCmdlineParser(self.config, self.prog, fullpath=self.fullpath) namespace_, args_ = new_parser._parse_known_args(args_, namespace) else: if self._registries['action']['help'].needs: # display help only when all variables assignemnt are done self._registries['action']['help'].needs = False helper = self._registries['action']['help'](None) helper.display(self) return namespace_, args_ def add_argument(self, *args, **kwargs): if args == ('-h', '--help'): super().add_argument(*args, **kwargs) else: raise NotImplementedError('do not use add_argument') def add_arguments(self, *args, **kwargs): raise NotImplementedError('do not use add_argument') def add_subparsers(self, *args, **kwargs): raise NotImplementedError('do not use add_subparsers') def _gen_argument(self, name, properties): if len(name) == 1 and 'longargument' not in properties: return self.prefix_chars + name return self.prefix_chars * 2 + name def _config_to_argparser(self, _forhelp: bool, config, prefix: Optional[str]=None, group=None) -> None: if group is None: group = super() actions = {} for obj in config.list(type='all'): option = obj.option if option.isoptiondescription(): if _forhelp: group = self.add_argument_group(option.doc()) if prefix: prefix_ = prefix + '.' + option.name() else: prefix_ = option.path() self._config_to_argparser(_forhelp, obj, prefix_, group) continue if 'frozen' in option.properties(): continue name = option.name() if name.startswith(self.prefix_chars): raise ValueError('name cannot startswith "{}"'.format(self.prefix_chars)) if self.fullpath and prefix: name = prefix + '.' + name properties = obj.property.get() kwargs = {'help': option.doc().replace('%', '%%')} if option.issymlinkoption(): actions[option.name(follow_symlink=True)][0].insert(0, self._gen_argument(option.name(), properties)) continue if 'positional' in properties: if not 'mandatory' in properties: raise ValueError('"positional" argument must be "mandatory" too') args = [option.path()] if _forhelp: kwargs['default'] = obj.value.default() else: kwargs['default'] = obj.value.get() kwargs['nargs'] = '?' else: kwargs['dest'] = option.path() kwargs['default'] = SUPPRESS args = [self._gen_argument(name, properties)] if _forhelp and 'mandatory' in properties: kwargs['required'] = True if option.type() == 'boolean': if 'mandatory' in properties: raise ValueError('"mandatory" property is not allowed for BoolOption') #if not isinstance(option.default(), bool): # raise ValueError('default value is mandatory for BoolOption') if obj.value.get() is False: action = 'store_true' else: action = 'store_false' kwargs['action'] = action else: if _forhelp: value = obj.value.default() else: value = obj.value.get() if value not in [None, []]: #kwargs['default'] = kwargs['const'] = option.default() #kwargs['action'] = 'store_const' kwargs['nargs'] = '?' if option.ismulti(): if _forhelp and 'mandatory' in properties: kwargs['nargs'] = '+' else: kwargs['nargs'] = '*' if option.type() == 'string': pass elif option.type() == 'integer': kwargs['type'] = int elif option.type() == 'choice': kwargs['choices'] = obj.value.list() else: pass #raise NotImplementedError('not supported yet') actions[option.name()] = (args, kwargs) for args, kwargs in actions.values(): group.add_argument(*args, **kwargs) def parse_args(self, *args, valid_mandatory=True, **kwargs): kwargs['namespace'] = TiramisuNamespace(self.config) try: namespaces = super().parse_args(*args, **kwargs) del namespaces.__dict__['_config'] except PropertiesOptionError as err: name = err._option_bag.option.impl_getname() properties = self.config.option(name).property.get() if self.fullpath and 'positional' not in properties: if len(name) == 1 and 'longargument' not in properties: name = self.prefix_chars + name else: name = self.prefix_chars * 2 + name if err.proptype == ['mandatory']: self.error('the following arguments are required: {}'.format(name)) else: self.error('unrecognized arguments: {}'.format(name)) if valid_mandatory: for key in self.config.value.mandatory(): properties = self.config.option(key).property.get() if 'positional' not in properties: if self.fullpath or '.' not in key: name = key else: name = key.rsplit('.', 1)[1] args = self._gen_argument(name, self.config.option(key).property.get()) else: args = key if not self.fullpath and '.' in args: args = args.rsplit('.', 1)[1] self.error('the following arguments are required: {}'.format(args)) return namespaces def format_usage(self, *args, **kwargs): help_formatter = TiramisuCmdlineParser(self.config, self.prog, fullpath=self.fullpath, _forhelp=True) return super(TiramisuCmdlineParser, help_formatter).format_usage(*args, **kwargs) def format_help(self, *args, **kwargs): help_formatter = TiramisuCmdlineParser(self.config, self.prog, fullpath=self.fullpath, _forhelp=True) return super(TiramisuCmdlineParser, help_formatter).format_help(*args, **kwargs) def get_config(self): return self.config