From 2e829c4cc31fc50a317c97a1bc881f1b4a70932f Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Tue, 23 Nov 2021 12:34:50 +0100 Subject: [PATCH] config: add ask_values method that allowing interactive configuration --- mylib/config.py | 173 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 3 deletions(-) diff --git a/mylib/config.py b/mylib/config.py index b8872fb..0ff8915 100644 --- a/mylib/config.py +++ b/mylib/config.py @@ -26,7 +26,7 @@ DEFAULT_ENCODING = 'utf-8' DEFAULT_CONFIG_DIRPATH = os.path.expanduser('./') -class BaseOption: +class BaseOption: # pylint: disable=too-many-instance-attributes """ Base configuration option class """ def __init__(self, config, section, name, default=None, comment=None, @@ -201,6 +201,28 @@ class BaseOption: lines.append('') return '\n'.join(lines) + def _ask_value(self, prompt=None): + """ Ask to user to enter value of this option and return it """ + if self.comment: + print('# ' + self.comment) + default_value = self.get() + if not prompt: + prompt = "%s: " % self.name + if default_value is not None: + prompt += "[%s] " % self.to_config(default_value) + value = input(prompt) + return default_value if value == '' else value + + def ask_value(self, set_it=True): + """ + Ask to user to enter value of this option and set or + return it regarding set parameter + """ + value = self._ask_value() + if set_it: + return self.set(value) + return value + class StringOption(BaseOption): """ String configuration option class """ @@ -238,6 +260,25 @@ class BooleanOption(BaseOption): ).lower().replace('_', '-') ) + def _ask_value(self, prompt=None): + """ Ask to user to enter value of this option and return it """ + default_value = self.get() + prompt = "%s: " % self.name + if default_value: + prompt += '[Y/n] ' + else: + prompt += '[y/N] ' + while True: + value = super()._ask_value(prompt).lower() + if value in ['', None]: + return default_value + if value == 'y': + return True + if value == 'n': + return False + + print('Invalid answer. Possible values: Y or N (case insensitive)') + class FloatOption(BaseOption): """ Float configuration option class """ @@ -251,6 +292,18 @@ class FloatOption(BaseOption): def parser_type(self): return float + def _ask_value(self, prompt=None): + """ Ask to user to enter value of this option and return it """ + default_value = self.get() + while True: + value = super()._ask_value() + if value in ['', None]: + return default_value + try: + return float(value) + except ValueError: + print('Invalid answer. Must a numeric value, for instance "12" or "12.5"') + class IntegerOption(BaseOption): """ Integer configuration option class """ @@ -269,6 +322,18 @@ class IntegerOption(BaseOption): def parser_type(self): return int + def _ask_value(self, prompt=None): + """ Ask to user to enter value of this option and return it """ + default_value = self.get() + while True: + value = super()._ask_value() + if value in ['', None]: + return default_value + try: + return int(value) + except ValueError: + print('Invalid answer. Must a integer value') + class PasswordOption(StringOption): """ Password configuration option class """ @@ -324,15 +389,57 @@ class PasswordOption(StringOption): return self.keyring_value return super().to_config(value) - def set(self, value): + def set(self, value, use_keyring=None): # pylint: disable=arguments-differ """ Set option value to config file """ - if super().get() == self.keyring_value: + if (use_keyring is None and super().get() == self.keyring_value) or use_keyring: keyring.set_password( self._keyring_service_name, self._keyring_username, value) value = self.keyring_value super().set(value) + def _ask_value(self, prompt=None): + """ Ask to user to enter value of this option and return it """ + if self.comment: + print('# ' + self.comment) + default_value = self.get() + if not prompt: + prompt = '%s: ' % self.name + if default_value is not None: + # Hide value only if it differed from default value + if default_value == self.default: + prompt += '[%s] ' % default_value + else: + prompt += '[secret defined, leave to empty to keep it as unchange] ' + value = getpass(prompt) + return default_value if value == '' else value + + def ask_value(self, set_it=True): + """ + Ask to user to enter value of this option and set or + return it regarding set parameter + """ + value = self._ask_value() + if set_it: + use_keyring = None + default_use_keyring = (super().get() == self.keyring_value) + while use_keyring is None: + prompt = ( + 'Do you want to use XDG keyring ? [%s] ' % + ('Y/n' if default_use_keyring else 'y/N') + ) + result = input(prompt).lower() + if result == '': + use_keyring = default_use_keyring + elif result == 'y': + use_keyring = True + elif result == 'n': + use_keyring = False + else: + print('Invalid answer. Possible values: Y or N (case insensitive)') + return self.set(value, use_keyring=use_keyring) + return value + class ConfigSection: """ Configuration section class """ @@ -395,6 +502,34 @@ class ConfigSection: lines.append(self.options[option].export_to_config()) return '\n'.join(lines) + def ask_values(self, set_it=True): + """ + Ask user to enter value for each configuration option of the section + + :param set_it: If True (default), option value will be updated with user input + + :return: If set_it is True, return True if valid value for each configuration + option have been retrieved and set. If False, return a dict of configuration + options and their value. + :rtype: bool of dict + """ + if self.comment: + print('# %s' % self.comment) + print('[%s]\n' % self.name) + result = dict() + error = False + for name, option in self.options.items(): + option_result = option.ask_value(set_it=set_it) + if set_it: + result[name] = option_result + elif not option_result: + error = True + print() + print() + if set_it: + return not error + return result + class RawWrappedTextHelpFormatter(argparse.RawDescriptionHelpFormatter): @@ -684,6 +819,38 @@ class Config: for section in self._ordered_section_names: self.sections[section].add_options_to_parser(parser) + def ask_values(self, set_it=True, execute_callback=False): + """ + Ask user to enter value for each configuration option + + :param set_it: If True (default), option value will be updated with user input + :param execute_callback: Sections's loaded callbacks will be finally executed + (only if set_it is True, default: False) + + :return: If set_it is True, return True if valid value for each configuration + option have been retrieved and set. If False, return a dict of configuration + section and their options value. + :rtype: bool of dict + """ + # On set it mode, ensure configuration file parser is initialized + if set_it and not self.config_parser: + self.config_parser = ConfigParser() + result = dict() + error = False + for name, section in self.sections.items(): + section_result = section.ask_values(set_it=set_it) + if not set_it: + result[name] = section_result + elif not section_result: + error = True + if set_it: + if error: + return False + if execute_callback: + self._loaded() + return True + return result + @property def config_dir(self): """ Retrieve configuration directory path """