simplify find() in api and PropertiesOptionError

This commit is contained in:
Emmanuel Garette 2018-04-10 22:42:20 +02:00
parent 5eb2f04202
commit 9e378faef0
11 changed files with 213 additions and 198 deletions

View file

@ -24,7 +24,7 @@ def return_calc_list(val):
return [val]
def return_error():
def return_error(*args, **kwargs):
raise Exception('test')
@ -83,6 +83,24 @@ def test_choiceoption_function_error():
raises(ConfigError, "api.option('choice').value.set('val1')")
def test_choiceoption_function_error_args():
choice = ChoiceOption('choice', '', values=return_error, values_params={'': ('val1',)})
odesc = OptionDescription('od', '', [choice])
cfg = Config(odesc)
api = getapi(cfg)
api.property.read_write()
raises(ConfigError, "api.option('choice').value.set('val1')")
def test_choiceoption_function_error_kwargs():
choice = ChoiceOption('choice', '', values=return_error, values_params={'kwargs': ('val1',)})
odesc = OptionDescription('od', '', [choice])
cfg = Config(odesc)
api = getapi(cfg)
api.property.read_write()
raises(ConfigError, "api.option('choice').value.set('val1')")
def test_choiceoption_calc_function():
choice = ChoiceOption('choice', "", values=return_calc_list, values_params={'': ('val1',)})
odesc = OptionDescription('od', '', [choice])

View file

@ -195,6 +195,11 @@ def test_find_in_config():
assert len(ret) == 1
_is_same_opt(ret[0].option.get(), api.option('gc.prop').option.get())
#
ret = api.option.find('prop', value=None)
ret = api.option.find('prop')
assert len(ret) == 1
_is_same_opt(ret[0].option.get(), api.option('gc.prop').option.get())
#
api.property.read_write()
raises(AttributeError, "assert api.option.find('prop').option.get()")
ret = api.unrestraint.option.find(name='prop')

View file

@ -48,6 +48,11 @@ def make_metaconfig(double=False):
return api
def test_unknown_config():
api = make_metaconfig()
raises(ConfigError, "api.config('unknown')")
#FIXME ne pas mettre 2 meta dans une config
#FIXME ne pas mettre 2 OD differents dans un meta
def test_none():

View file

@ -10,6 +10,7 @@ from tiramisu import ChoiceOption, BoolOption, IntOption, FloatOption, \
getapi, undefined
from tiramisu.api import TIRAMISU_VERSION
from tiramisu.error import PropertiesOptionError, ConflictError, SlaveError, ConfigError
from tiramisu.i18n import _
def return_val():
@ -652,7 +653,13 @@ def test_consistency_master_and_slaves_master_mandatory_transitive():
maconfig = OptionDescription('rootconfig', '', [interface1, interface2])
api = getapi(Config(maconfig))
api.property.read_write()
raises(PropertiesOptionError, "api.option('val1.val1').value.get()")
err = None
try:
api.option('val1.val1').value.get()
except PropertiesOptionError as error:
err = error
assert err, 'should raises'
assert str(err) == str(_('cannot access to {0} "{1}" because "{2}" has {3} {4}').format('option', 'val1', 'val2', 'property', '"disabled"'))
raises(PropertiesOptionError, "api.option('val3.val3').value.get()")
assert list(api.value.mandatory_warnings()) == []

View file

@ -3,6 +3,7 @@ from .autopath import do_autopath
do_autopath()
from copy import copy
from tiramisu.i18n import _
from tiramisu.setting import groups
from tiramisu import setting
setting.expires_time = 1
@ -93,16 +94,17 @@ def test_requires_invalid():
def test_requires_same_action():
a = BoolOption('activate_service', '', True)
b = BoolOption('activate_service_web', '', True,
requires=[{'option': a, 'expected': False, 'action': 'new'}])
activate_service = BoolOption('activate_service', '', True)
activate_service_web = BoolOption('activate_service_web', '', True,
requires=[{'option': activate_service, 'expected': False,
'action': 'new'}])
d = IPOption('ip_address_service_web', '',
requires=[{'option': b, 'expected': False,
'action': 'disabled', 'inverse': False,
'transitive': True, 'same_action': False}])
od = OptionDescription('service', '', [a, b, d])
api = getapi(Config(od))
ip_address_service_web = IPOption('ip_address_service_web', '',
requires=[{'option': activate_service_web, 'expected': False,
'action': 'disabled', 'inverse': False,
'transitive': True, 'same_action': False}])
od1 = OptionDescription('service', '', [activate_service, activate_service_web, ip_address_service_web])
api = getapi(Config(od1))
api.property.read_write()
api.property.add('new')
api.option('activate_service').value.get()
@ -122,6 +124,8 @@ def test_requires_same_action():
api.option('ip_address_service_web').value.get()
except PropertiesOptionError as err:
props = err.proptype
submsg = '"disabled" (' + _('the value of "{0}" is {1}').format('activate_service', '"False"') + ')'
assert str(err) == str(_('cannot access to {0} "{1}" because has {2} {3}').format('option', 'ip_address_service_web', 'property', submsg))
assert frozenset(props) == frozenset(['disabled'])

View file

@ -662,7 +662,6 @@ class TiramisuOption(CommonTiramisu):
for path in self.config_bag.config.find(byname=name,
byvalue=value,
bytype=None,
type_='path',
_subpath=self.path,
config_bag=self.config_bag):
config_bag = self.config_bag.copy('nooption')
@ -900,7 +899,6 @@ class TiramisuContextOption(TiramisuContext):
for path in self.config_bag.config.find(byname=name,
byvalue=value,
bytype=None,
type_='path',
#_subpath=self.path,
config_bag=self.config_bag):
config_bag = self.config_bag.copy('nooption')

View file

@ -185,7 +185,7 @@ class SubConfig(object):
`setting.groups`
"""
if group_type is not None and not isinstance(group_type,
groups.GroupType): # pragma: optional cover
groups.GroupType):
raise TypeError(_("unknown group_type: {0}").format(group_type))
for child in self.cfgimpl_get_description().impl_getchildren(config_bag):
if child.impl_is_optiondescription():
@ -199,7 +199,7 @@ class SubConfig(object):
yield name, self.getattr(name,
None,
nconfig_bag)
except PropertiesOptionError: # pragma: optional cover
except PropertiesOptionError:
pass
def cfgimpl_get_children(self,
@ -227,12 +227,12 @@ class SubConfig(object):
old `SubConfig`, `Values`, `Multi` or `Settings`)
"""
context = self._impl_context()
if context is None: # pragma: optional cover
if context is None: # pragma: no cover
raise ConfigError(_('the context does not exist anymore'))
return context
def cfgimpl_get_description(self):
if self._impl_descr is None: # pragma: optional cover
if self._impl_descr is None:
raise ConfigError(_('no option description found for this config'
' (may be GroupConfig)'))
else:
@ -252,7 +252,7 @@ class SubConfig(object):
_commit=True):
context = self.cfgimpl_get_context()
if '.' in name: # pragma: optional cover
if '.' in name:
# when set_value
self, name = self.cfgimpl_get_home_by_path(name,
config_bag)
@ -281,7 +281,7 @@ class SubConfig(object):
name,
index,
config_bag):
if '.' in name: # pragma: optional cover
if '.' in name:
self, name = self.cfgimpl_get_home_by_path(name,
config_bag)
option = config_bag.option
@ -385,7 +385,6 @@ class SubConfig(object):
byname,
byvalue,
config_bag,
type_='option',
_subpath=None,
raise_if_not_found=True,
only_path=undefined,
@ -397,8 +396,6 @@ class SubConfig(object):
:return: find list or an exception if nothing has been found
"""
def _filter_by_value(sconfig_bag):
if byvalue is undefined:
return True
try:
value = self.getattr(path,
None,
@ -410,9 +407,6 @@ class SubConfig(object):
else:
return value == byvalue
if type_ not in ('option', 'path', 'value'): # pragma: optional cover
raise ValueError(_('unknown type_ type {0}'
'for find').format(type_))
found = False
if only_path is not undefined:
options = [(only_path, only_option)]
@ -424,10 +418,10 @@ class SubConfig(object):
for path, option in options:
sconfig_bag = config_bag.copy('nooption')
sconfig_bag.option = option
if not _filter_by_value(sconfig_bag):
if byvalue is not undefined and not _filter_by_value(sconfig_bag):
continue
#remove option with propertyerror, ...
if sconfig_bag.validate_properties:
elif sconfig_bag.validate_properties:
#remove option with propertyerror, ...
try:
self.unwrap_from_path(path,
sconfig_bag)
@ -436,18 +430,10 @@ class SubConfig(object):
sconfig_bag)
except PropertiesOptionError:
continue
if type_ == 'value':
retval = self.getattr(path,
None,
sconfig_bag)
elif type_ == 'path':
retval = path
elif type_ == 'option':
retval = option
found = True
yield retval
return self._find_return_results(found,
raise_if_not_found)
yield path
self._find_return_results(found,
raise_if_not_found)
def _find_return_results(self,
found,
@ -502,7 +488,7 @@ class SubConfig(object):
pathsvalues = []
if _currpath is None:
_currpath = []
if withoption is None and withvalue is not undefined: # pragma: optional cover
if withoption is None and withvalue is not undefined:
raise ValueError(_("make_dict can't filtering with value without "
"option"))
context = self.cfgimpl_get_context()
@ -510,7 +496,6 @@ class SubConfig(object):
for path in context.find(bytype=None,
byname=withoption,
byvalue=withvalue,
type_='path',
_subpath=self.cfgimpl_get_path(False),
config_bag=config_bag):
path = '.'.join(path.split('.')[:-1])
@ -526,7 +511,7 @@ class SubConfig(object):
break
else:
tmypath = mypath + '.'
if not path.startswith(tmypath): # pragma: optional cover
if not path.startswith(tmypath):
raise AttributeError(_('unexpected path {0}, '
'should start with {1}'
'').format(path, mypath))
@ -751,7 +736,7 @@ class Config(_CommonConfig):
properties, permissives, values, session_id = get_storages(self,
session_id,
persistent)
if not valid_name(session_id): # pragma: optional cover
if not valid_name(session_id):
raise ValueError(_("invalid session ID: {0} for config").format(session_id))
self._impl_settings = Settings(self,
properties,
@ -869,7 +854,15 @@ class GroupConfig(_CommonConfig):
nconfig_bag,
_commit=False)
except PropertiesOptionError as err:
ret.append(PropertiesOptionError(str(err), err.proptype))
ret.append(PropertiesOptionError(err._path,
err._index,
err._config_bag,
err.proptype,
err._settings,
err._opt_type,
err._requires,
err._name,
err._orig_opt))
except (ValueError, SlaveError) as err:
ret.append(err)
if _commit:
@ -896,7 +889,6 @@ class GroupConfig(_CommonConfig):
byvalue=undefined,
byname=byname,
config_bag=config_bag,
type_='path',
raise_if_not_found=raise_if_not_found))
byname = None
byoption = self.cfgimpl_get_description().impl_get_opt_by_path(bypath)
@ -918,7 +910,6 @@ class GroupConfig(_CommonConfig):
next(child.find(None,
byname,
byvalue,
type_='path',
config_bag=config_bag,
raise_if_not_found=False,
only_path=bypath,
@ -935,22 +926,13 @@ class GroupConfig(_CommonConfig):
def impl_getname(self):
return self._impl_name
# def __str__(self):
# ret = ''
# for child in self._impl_children:
# ret += "({0})\n".format(child._impl_name)
# if self._impl_descr is not None:
# ret += super(GroupConfig, self).__str__()
# return ret
#
# __repr__ = __str__
def getconfig(self,
name):
for child in self._impl_children:
if name == child.impl_getname():
return child
raise ConfigError(_('unknown config {}').format(name))
raise ConfigError(_('unknown config "{}"').format(name))
class MetaConfig(GroupConfig):

View file

@ -55,27 +55,48 @@ def display_list(lst, separator='and', add_quote=False):
class PropertiesOptionError(AttributeError):
"attempt to access to an option with a property that is not allowed"
def __init__(self,
msg,
path,
index,
config_bag,
proptype,
settings=None,
datas=None,
option_type=None):
settings,
opt_type=None,
requires=None,
name=None,
orig_opt=None):
self._path = path
self._index = index
if opt_type:
self._opt_type = opt_type
self._requires = requires
self._name = name
self._orig_opt = orig_opt
else:
if config_bag.option.impl_is_optiondescription():
self._opt_type = 'optiondescription'
else:
self._opt_type = 'option'
self._requires = config_bag.option.impl_getrequires()
self._name = config_bag.option.impl_get_display_name()
self._orig_opt = None
self._config_bag = config_bag.copy('nooption')
self.proptype = proptype
self._settings = settings
self._datas = datas
self._type = option_type
self._orig_opt = None
super(PropertiesOptionError, self).__init__(msg)
self.msg = None
super(PropertiesOptionError, self).__init__(None)
def set_orig_opt(self, opt):
self._orig_opt = opt
def __str__(self):
#this part is a bit slow, so only execute when display
if self._settings is None:
req = {}
else:
req = self._settings.apply_requires(**self._datas)
if self.msg:
return self.msg
req = self._settings.apply_requires(self._path,
self._requires,
self._index,
True,
self._config_bag)
#if req != {} or self._orig_opt is not None:
if req != {}:
only_one = len(req) == 1
@ -91,17 +112,20 @@ class PropertiesOptionError(AttributeError):
else:
prop_msg = _('properties')
if self._orig_opt:
return str(_('cannot access to {0} "{1}" because "{2}" has {3} {4}'
'').format(self._type,
self._orig_opt.impl_get_display_name(),
self._datas['config_bag'].option.impl_get_display_name(),
self.msg = str(_('cannot access to {0} "{1}" because "{2}" has {3} {4}'
'').format(self._opt_type,
self._orig_opt.impl_get_display_name(),
self._name,
prop_msg,
msg))
self.msg = str(_('cannot access to {0} "{1}" because has {2} {3}'
'').format(self._opt_type,
self._name,
prop_msg,
msg))
return str(_('cannot access to {0} "{1}" because has {2} {3}'
'').format(self._type,
self._datas['config_bag'].option.impl_get_display_name(),
prop_msg,
msg))
del self._path, self._index, self._requires, self._opt_type, self._name, self._config_bag
del self._settings, self._orig_opt
return self.msg
#____________________________________________________________

View file

@ -25,7 +25,7 @@ from inspect import signature
from ..i18n import _
from ..setting import undefined
from ..error import ConfigError
from ..error import ConfigError, display_list
STATIC_TUPLE = frozenset()
@ -162,7 +162,7 @@ class Base(object):
set_forbidden_properties = calc_properties & properties
if set_forbidden_properties != frozenset():
raise ValueError(_('conflict: properties already set in '
'requirement {0}').format(list(set_forbidden_properties)))
'requirement {0}').format(display_list(list(set_forbidden_properties))))
_setattr = object.__setattr__
_setattr(self, '_name', name)
_setattr(self, '_informations', {'doc': doc})
@ -249,10 +249,9 @@ class Base(object):
if calculator_args or calculator_kwargs:
# there is more args/kwargs than expected!
raise ConfigError(_('cannot find those arguments "{}" in function "{}" for "{}"'
'').format(list(calculator_args | calculator_kwargs),
'').format(display_list(list(calculator_args | calculator_kwargs)),
calculator.__name__,
self.impl_get_display_name()))
has_self = False
has_index = False
if is_multi and func_args:
# there is extra args/kwargs
@ -267,16 +266,13 @@ class Base(object):
has_index = True
params.append(('index',))
func_args.pop()
if func_args:
raise ConfigError(_('missing those arguements "{}" in function "{}" for "{}"'
'').format(list(func_args),
calculator.__name__,
self.impl_get_display_name()))
calculator_params[''] = tuple(params)
if func_args:
raise ConfigError(_('missing those arguments "{}" in function "{}" for "{}"'
'').format(display_list(list(func_args)),
calculator.__name__,
self.impl_get_display_name()))
if not self.impl_is_optiondescription() and self.impl_is_multi():
if add_value and not has_self and 'self' in func_kwargs:
# only for validator
calculator_params['self'] = (self, False)
if not has_index and 'index' in func_kwargs:
calculator_params['index'] = (('index',),)
return calculator_params
@ -573,8 +569,9 @@ def validate_requires_arg(new_option,
option = exp['option']
option._add_dependency(new_option)
if option is not None:
err = option._validate(exp['value'], undefined)
if err:
try:
option._validate(exp['value'], undefined)
except ValueError as err:
raise ValueError(_('malformed requirements expected value '
'must be valid for option {0}'
': {1}').format(name, err))
@ -588,11 +585,13 @@ def validate_requires_arg(new_option,
else:
option = get_option(require)
if expected is not None:
err = option._validate(expected, undefined)
if err:
try:
option._validate(expected, undefined)
except ValueError as err:
raise ValueError(_('malformed requirements expected value '
'must be valid for option {0}'
': {1}').format(name, err))
option._add_dependency(new_option)
_set_expected(action,
inverse,
transitive,

View file

@ -373,6 +373,7 @@ class Settings(object):
apply_requires)
if apply_requires:
props |= self.apply_requires(path,
opt.impl_getrequires(),
index,
False,
config_bag)
@ -405,9 +406,11 @@ class Settings(object):
def apply_requires(self,
path,
current_requires,
index,
debug,
config_bag):
readable,
config_bag,
name=None):
"""carries out the jit (just in time) requirements between options
a requirement is a tuple of this form that comes from the option's
@ -451,11 +454,10 @@ class Settings(object):
:param path: the option's path in the config
:type path: str
"""
opt = config_bag.option
current_requires = opt.impl_getrequires()
#current_requires = opt.impl_getrequires()
# filters the callbacks
if debug:
if readable:
calc_properties = {}
else:
calc_properties = set()
@ -467,25 +469,21 @@ class Settings(object):
all_properties = None
for requires in current_requires:
for require in requires:
exps, action, inverse, \
transitive, same_action, operator = require
exps, action, inverse, transitive, same_action, operator = require
breaked = False
for exp in exps:
option, expected = exp
for option, expected in exps:
reqpath = option.impl_getpath(context)
if reqpath == path or reqpath.startswith(path + '.'): # pragma: optional cover
#FIXME c'est un peut tard !
if reqpath == path or reqpath.startswith(path + '.'):
raise RequirementError(_("malformed requirements "
"imbrication detected for option:"
" '{0}' with requirement on: "
"'{1}'").format(path, reqpath))
if not option.impl_is_multi():
idx = None
is_indexed = False
elif option.impl_is_master_slaves('slave'):
idx = None
is_indexed = False
if option.impl_is_master_slaves('slave'):
idx = index
is_indexed = False
else:
idx = None
elif option.impl_is_multi():
is_indexed = True
sconfig_bag = config_bag.copy('nooption')
sconfig_bag.option = option
@ -497,40 +495,44 @@ class Settings(object):
if is_indexed:
value = value[index]
except PropertiesOptionError as err:
properties = err.proptype
if not transitive:
if all_properties is None:
all_properties = []
for requires_ in opt.impl_getrequires():
for requires_ in current_requires:
for require_ in requires_:
all_properties.append(require_[1])
if not set(err.proptype) - set(all_properties):
if not set(properties) - set(all_properties):
continue
properties = err.proptype
if same_action and action not in properties: # pragma: optional cover
if same_action and action not in properties:
if len(properties) == 1:
prop_msg = _('property')
else:
prop_msg = _('properties')
raise RequirementError(_('cannot access to option "{0}" because '
'required option "{1}" has {2} {3}'
'').format(opt.impl_get_display_name(),
'').format(name,
option.impl_get_display_name(),
prop_msg,
display_list(list(properties))))
orig_value = err
# transitive action, force expected
value = expected[0]
inverse = False
else:
orig_value = value
if (not inverse and value in expected or
inverse and value not in expected):
# transitive action, add action
if operator != 'and':
if debug:
if isinstance(orig_value, PropertiesOptionError):
for msg in orig_value._settings.apply_requires(**orig_value._datas).values():
calc_properties.setdefault(action, []).extend(msg)
else:
if readable:
for msg in self.apply_requires(err.path,
err.requires,
err.index,
True,
err.config_bag).values():
calc_properties.setdefault(action, []).extend(msg)
else:
calc_properties.add(action)
breaked = True
break
else:
if (not inverse and value in expected or
inverse and value not in expected):
if operator != 'and':
if readable:
if not inverse:
msg = _('the value of "{0}" is {1}')
else:
@ -538,12 +540,12 @@ class Settings(object):
calc_properties.setdefault(action, []).append(
msg.format(option.impl_get_display_name(),
display_list(expected, 'or', add_quote=True)))
else:
calc_properties.add(action)
breaked = True
break
elif operator == 'and':
break
else:
calc_properties.add(action)
breaked = True
break
elif operator == 'and':
break
else:
if operator == 'and':
calc_properties.add(action)
@ -691,11 +693,6 @@ class Settings(object):
config_bag)
config_bag.properties = self_properties
properties = self_properties & config_bag.setting_properties - {'frozen', 'mandatory', 'empty'}
if not opt.impl_is_optiondescription():
opt_type = 'option'
else:
opt_type = 'optiondescription'
# remove permissive properties
if (config_bag.force_permissive is True or 'permissive' in config_bag.setting_properties) and properties:
@ -703,15 +700,11 @@ class Settings(object):
properties -= self.get_context_permissive()
# at this point an option should not remain in properties
if properties != frozenset():
datas = {'path': path,
'config_bag': config_bag,
'index': index,
'debug': True}
raise PropertiesOptionError(None,
raise PropertiesOptionError(path,
index,
config_bag,
properties,
self,
datas,
opt_type)
self)
def validate_mandatory(self,
path,
@ -735,17 +728,15 @@ class Settings(object):
index=index):
is_mandatory = True
if is_mandatory:
datas = {'path': path,
'config_bag': config_bag,
'index': index,
'debug': True}
raise PropertiesOptionError(None,
raise PropertiesOptionError(path,
index,
config_bag,
['mandatory'],
self,
datas,
'option')
self)
def validate_frozen(self,
path,
index,
config_bag):
if config_bag.setting_properties and \
('everything_frozen' in config_bag.setting_properties or
@ -753,7 +744,11 @@ class Settings(object):
not ((config_bag.force_permissive is True or
'permissive' in config_bag.setting_properties) and
'frozen' in self.get_context_permissive()):
return True
raise PropertiesOptionError(path,
index,
config_bag,
['frozen'],
self)
return False
#____________________________________________________________
# read only/read write

View file

@ -56,7 +56,7 @@ class Values(object):
old `SubConfig`, `Values`, `Multi` or `Settings`)
"""
context = self.context()
if context is None:
if context is None: # pragma: no cover
raise ConfigError(_('the context does not exist anymore'))
return context
@ -242,7 +242,7 @@ class Values(object):
# if value is a list and index is set
if opt.impl_is_submulti() and (value == [] or not isinstance(value[0], list)):
# return value only if it's a submulti and not a list of list
_reset_cache(value,)
_reset_cache(value)
return value
if len(value) > index:
@ -251,28 +251,20 @@ class Values(object):
return value[index]
# there is no calculate value for this index,
# so return an other default value
elif isinstance(value, list):
# value is a list, but no index specified
if opt.impl_is_submulti() and (value != [] and not isinstance(value[0], list)):
# if submulti, return a list of value
value = [value]
_reset_cache(value)
return value
# otherwise just return the value
return value
elif index is not None:
# if not list but with index
if opt.impl_is_submulti():
# if submulti, return a list of value
value = [value]
_reset_cache(value)
return value
else:
# not a list or index is None
if opt.impl_is_submulti():
# return a list of list for a submulti
value = [[value]]
elif opt.impl_is_multi():
if isinstance(value, list):
# value is a list, but no index specified
if (value != [] and not isinstance(value[0], list)):
# if submulti, return a list of value
value = [value]
elif index is not None:
# if submulti, return a list of value
value = [value]
else:
# return a list of list for a submulti
value = [[value]]
elif opt.impl_is_multi() and not isinstance(value, list) and index is None:
# return a list for a multi
value = [value]
_reset_cache(value)
@ -375,16 +367,9 @@ class Values(object):
config_bag)
config_bag.properties = self_properties
opt = config_bag.option
if settings.validate_frozen(config_bag):
datas = {'path': path,
'config_bag': config_bag,
'index': index,
'debug': True}
raise PropertiesOptionError(None,
['frozen'],
settings,
datas,
'option')
settings.validate_frozen(path,
index,
config_bag)
settings.validate_mandatory(path,
index,
value,
@ -673,16 +658,9 @@ class Values(object):
None,
config_bag)
config_bag.properties = self_properties
if settings.validate_frozen(config_bag):
datas = {'path': path,
'config_bag': config_bag,
'index': index,
'debug': True}
raise PropertiesOptionError(None,
['frozen'],
settings,
datas,
'option')
settings.validate_frozen(path,
index,
config_bag)
#______________________________________________________________________
# information