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