Compare commits

..

2 commits

Author SHA1 Message Date
Benjamin Renard
eb183b0d3b config: split console logging between stdout & stderr base on level
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-06 18:19:48 +01:00
Benjamin Renard
55782df47c config: code cleaning 2023-01-06 18:19:48 +01:00
2 changed files with 96 additions and 51 deletions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-lines
""" Configuration & options parser """
@ -136,14 +137,17 @@ class BaseOption: # pylint: disable=too-many-instance-attributes
@property
def parser_dest(self):
""" Get option name in arguments parser options """
return '{0}_{1}'.format(self.section.name, self.name)
return f'{self.section.name}_{self.name}'
@property
def parser_help(self):
""" Get option help message in arguments parser options """
if self.arg_help and self.default is not None:
# pylint: disable=consider-using-f-string
return '{0} (Default: {1})'.format(
self.arg_help, re.sub(r'%([^%])', r'%%\1', str(self.default)))
self.arg_help,
re.sub(r'%([^%])', r'%%\1', str(self._default_in_config))
)
if self.arg_help:
return self.arg_help
return None
@ -153,9 +157,7 @@ class BaseOption: # pylint: disable=too-many-instance-attributes
""" Get option argument name in parser options """
return (
self.arg if self.arg else
'--{0}-{1}'.format(
self.section.name, self.name
).lower().replace('_', '-')
f'--{self.section.name}-{self.name}'.lower().replace('_', '-')
)
def add_option_to_parser(self, section_opts):
@ -292,6 +294,7 @@ class BooleanOption(BaseOption):
@property
def parser_argument_name(self):
""" Get option argument name in parser options """
# pylint: disable=consider-using-f-string
return (
self.arg if self.arg else
'--{0}-{1}-{2}'.format(
@ -304,7 +307,7 @@ class BooleanOption(BaseOption):
def _ask_value(self, prompt=None, **kwargs):
""" Ask to user to enter value of this option and return it """
default_value = self.get()
prompt = "%s: " % self.name
prompt = f'{self.name}: '
if default_value:
prompt += '[Y/n] '
else:
@ -413,12 +416,11 @@ class PasswordOption(StringOption):
value = keyring.get_password(service_name, username)
if value is None:
# pylint: disable=consider-using-f-string
value = getpass(
'Please enter %s%s: ' % (
self.comment if self.comment else
'%s %s' % (self.section.name, self.name),
(' for %s' % username)
if username != self.name else ''
'Please enter {0}{1}: '.format(
f'{self.section.name} {self.name}',
f' for {username}' if username != self.name else ''
)
)
keyring.set_password(service_name, username, value)
@ -445,11 +447,11 @@ class PasswordOption(StringOption):
print('# ' + self.comment)
default_value = kwargs.pop('default_value', self.get())
if not prompt:
prompt = '%s: ' % self.name
prompt = f'{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
prompt += f'[{default_value}] '
else:
prompt += '[secret defined, leave to empty to keep it as unchange] '
value = getpass(prompt)
@ -466,8 +468,8 @@ class PasswordOption(StringOption):
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')
'Do you want to use XDG keyring ? '
f"[{'Y/n' if default_use_keyring else 'y/N'}] "
)
result = input(prompt).lower()
if result == '':
@ -488,7 +490,7 @@ class ConfigSection:
def __init__(self, config, name, comment=None, order=None):
self.config = config
self.name = name
self.options = dict()
self.options = {}
self.comment = comment
self.order = order if isinstance(order, int) else 10
@ -500,7 +502,7 @@ class ConfigSection:
:param name: Option name
:param **kwargs: Dict of raw option for type class
"""
assert not self.defined(name), "Duplicated option %s" % name
assert not self.defined(name), f'Duplicated option {name}'
self.options[name] = _type(self.config, self, name, **kwargs)
return self.options[name]
@ -514,12 +516,12 @@ class ConfigSection:
def get(self, option):
""" Get option value """
assert self.defined(option), "Option %s unknown" % option
assert self.defined(option), f'Option {option} unknown'
return self.options[option].get()
def set(self, option, value):
""" Set option value """
assert self.defined(option), "Option %s unknown" % option
assert self.defined(option), f'Option {option} unknown'
return self.options[option].set(value)
def add_options_to_parser(self, parser):
@ -530,16 +532,16 @@ class ConfigSection:
else self.name.capitalize()
)
for option in self.options:
for option in self.options: # pylint: disable=consider-using-dict-items
self.options[option].add_option_to_parser(section_opts)
def export_to_config(self):
""" Export section and their options to configuration file """
lines = []
if self.comment:
lines.append('# %s' % self.comment)
lines.append('[%s]' % self.name)
for option in self.options:
lines.append(f'# {self.comment}')
lines.append(f'[{self.name}]')
for option in self.options: # pylint: disable=consider-using-dict-items
lines.append(self.options[option].export_to_config())
return '\n'.join(lines)
@ -555,9 +557,9 @@ class ConfigSection:
:rtype: bool of dict
"""
if self.comment:
print('# %s' % self.comment)
print('[%s]\n' % self.name)
result = dict()
print(f'# {self.comment}')
print(f'[{self.name}]''\n')
result = {}
error = False
for name, option in self.options.items():
option_result = option.ask_value(set_it=set_it)
@ -622,7 +624,7 @@ class Config: # pylint: disable=too-many-instance-attributes
The specified callback method will receive Config object as parameter.
: param ** kwargs: Raw parameters dict pass to ConfigSection __init__() method
"""
assert name not in self.sections, "Duplicated section %s" % name
assert name not in self.sections, f'Duplicated section {name}'
self.sections[name] = ConfigSection(self, name, **kwargs)
if loaded_callback:
@ -644,7 +646,7 @@ class Config: # pylint: disable=too-many-instance-attributes
""" Get option value """
assert self.config_parser or self.options, 'Unconfigured options parser'
assert self.defined(
section, option), 'Unknown option %s.%s' % (section, option)
section, option), f'Unknown option {section}.{option}'
value = self.sections[section].get(option)
log.debug('get(%s, %s): %s (%s)', section, option, value, type(value))
return value
@ -664,7 +666,7 @@ class Config: # pylint: disable=too-many-instance-attributes
""" Set option value """
assert self.config_parser, 'Unconfigured options parser'
assert self.defined(
section, option), 'Unknown option %s.%s' % (section, option)
section, option), f'Unknown option {section}.{option}'
self.sections[section].set(option, value)
def load_file(self, filepath, execute_callback=True):
@ -734,7 +736,11 @@ class Config: # pylint: disable=too-many-instance-attributes
'Configuration directory "%s" does not exist (or not writable)', dirpath)
return False
lines = ['#\n# %s configuration\n#\n' % self.appname]
lines = [
'#\n'
f'# {self.appname} configuration'
'\n#\n'
]
for section_name in self._ordered_section_names:
lines.append('')
@ -769,9 +775,15 @@ class Config: # pylint: disable=too-many-instance-attributes
formatter_class=RawWrappedTextHelpFormatter,
**kwargs)
config_file_help = 'Configuration file to use (default: %s)' % self.config_filepath
config_file_help = (
f'Configuration file to use (default: {self.config_filepath})'
)
if self.config_file_env_variable:
config_file_help += '\n\nYou also could set %s environment variable to specify your configuration file path.' % self.config_file_env_variable
config_file_help += (
'\n\nYou also could set '
f'{self.config_file_env_variable} environment variable to '
'specify your configuration file path.'
)
self.options_parser.add_argument(
'-c',
'--config',
@ -854,7 +866,7 @@ class Config: # pylint: disable=too-many-instance-attributes
# Load configuration file
if os.path.isfile(options.config) and not self.load_file(options.config, execute_callback=False):
parser.error(
'Failed to load configuration from file %s' % options.config
f'Failed to load configuration from file {options.config}'
)
if options.save and not already_saved:
@ -867,11 +879,20 @@ class Config: # pylint: disable=too-many-instance-attributes
logging.getLogger().setLevel(logging.INFO)
if self.get('console', 'enabled'):
console_handler = logging.StreamHandler(sys.stdout)
stdout_console_handler = logging.StreamHandler(sys.stdout)
stdout_console_handler.addFilter(StdoutInfoFilter())
stdout_console_handler.setLevel(logging.DEBUG)
stderr_console_handler = logging.StreamHandler(sys.stderr)
stderr_console_handler.setLevel(logging.WARNING)
if self.get('console', 'log_format'):
console_formater = logging.Formatter(self.get('console', 'log_format'))
console_handler.setFormatter(console_formater)
logging.getLogger().addHandler(console_handler)
stdout_console_handler.setFormatter(console_formater)
stderr_console_handler.setFormatter(console_formater)
logging.getLogger().addHandler(stdout_console_handler)
logging.getLogger().addHandler(stderr_console_handler)
if execute_callback:
self._loaded()
@ -907,7 +928,7 @@ class Config: # pylint: disable=too-many-instance-attributes
# On set it mode, ensure configuration file parser is initialized
if set_it and not self.config_parser:
self.config_parser = ConfigParser()
result = dict()
result = {}
error = False
for name, section in self.sections.items():
section_result = section.ask_values(set_it=set_it)
@ -960,14 +981,14 @@ class Config: # pylint: disable=too-many-instance-attributes
argv, create=False, execute_callback=False)
if os.path.exists(options.config) and not options.overwrite:
print('Configuration file %s already exists' % options.config)
print(f'Configuration file {options.config} already exists')
sys.exit(1)
if options.interactive:
self.ask_values(set_it=True)
if self.save(options.config):
print('Configuration file %s created.' % options.config)
print(f'Configuration file {options.config} created.')
if options.validate:
print('Validate your configuration...')
try:
@ -976,14 +997,15 @@ class Config: # pylint: disable=too-many-instance-attributes
else:
print('Error(s) occurred validating your configuration. See logs for details.')
sys.exit(1)
except Exception:
except Exception: # pylint: disable=broad-except
print(
'Exception occurred validating your configuration: %s\n\nSee logs for details.' %
traceback.format_exc())
'Exception occurred validating your configuration:\n'
f'{traceback.format_exc()}'
'\n\nSee logs for details.'
)
sys.exit(2)
else:
print('Error occured creating configuration file %s' %
options.config)
print(f'Error occured creating configuration file {options.config}')
sys.exit(1)
sys.exit(0)
@ -1006,8 +1028,8 @@ class Config: # pylint: disable=too-many-instance-attributes
return os.environ.get(self.config_file_env_variable)
return os.path.join(
self.config_dir,
("%s.ini" % self.shortname) if self.shortname
else "config.ini"
f'{self.shortname}.ini' if self.shortname
else 'config.ini'
)
@ -1044,7 +1066,7 @@ class ConfigurableObject:
**kwargs):
for key, value in kwargs.items():
assert key in self._defaults, "Unknown %s option" % key
assert key in self._defaults, f'Unknown {key} option'
self._kwargs[key] = value
if options:
@ -1054,7 +1076,7 @@ class ConfigurableObject:
elif self._config_name:
self._options_prefix = self._config_name + '_'
else:
raise Exception('No configuration name defined for %s' % __name__)
raise Exception(f'No configuration name defined for {__name__}')
if config:
self._config = config
@ -1063,7 +1085,7 @@ class ConfigurableObject:
elif self._config_name:
self._config_section = self._config_name
else:
raise Exception('No configuration name defined for %s' % __name__)
raise Exception(f'No configuration name defined for {__name__}')
def _get_option(self, option, default=None, required=False):
""" Retreive option value """
@ -1076,7 +1098,7 @@ class ConfigurableObject:
if self._config and self._config.defined(self._config_section, option):
return self._config.get(self._config_section, option)
assert not required, "Options %s not defined" % option
assert not required, f'Options {option} not defined'
return default if default is not None else self._defaults.get(option)
@ -1114,3 +1136,10 @@ class ConfigSectionAsDictWrapper:
def __delitem__(self, key):
raise Exception('Deleting a configuration option is not supported')
# pylint: disable=too-few-public-methods
class StdoutInfoFilter(logging.Filter):
""" Logging filter to keep messages only >= logging.INFO """
def filter(self, record):
return record.levelno in (logging.DEBUG, logging.INFO)

View file

@ -1,10 +1,12 @@
# pylint: disable=redefined-outer-name,missing-function-docstring,protected-access,global-statement
""" Tests on config lib """
import logging
from mylib.config import Config, ConfigSection
from mylib.config import StringOption
runned = dict()
runned = {}
def test_config_init_default_args():
@ -208,3 +210,17 @@ def test_get_default():
config.parse_arguments_options(argv=[], create=False)
assert config.get(section_name, opt_name) == opt_default_value
def test_logging_splited_stdout_stderr(capsys):
config = Config('Test app')
config.parse_arguments_options(argv=['-C', '-v'], create=False)
info_msg = "[info]"
err_msg = "[error]"
logging.getLogger().info(info_msg)
logging.getLogger().error(err_msg)
captured = capsys.readouterr()
assert info_msg in captured.out
assert info_msg not in captured.err
assert err_msg in captured.err
assert err_msg not in captured.out