653 lines
26 KiB
Python
653 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (C) 2014-2017 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# The original `Config` design model is unproudly borrowed from
|
|
# the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
|
|
# the whole pypy projet is under MIT licence
|
|
# ____________________________________________________________
|
|
import re
|
|
from types import FunctionType
|
|
import sys
|
|
|
|
from ..i18n import _
|
|
from ..setting import undefined
|
|
from ..error import ConfigError
|
|
|
|
if sys.version_info[0] >= 3: # pragma: no cover
|
|
from inspect import signature
|
|
else:
|
|
from inspect import getargspec
|
|
|
|
STATIC_TUPLE = tuple()
|
|
|
|
|
|
submulti = 2
|
|
NAME_REGEXP = re.compile(r'^[a-z][a-zA-Z\d_]*$')
|
|
FORBIDDEN_NAMES = frozenset(['iter_all', 'iter_group', 'find', 'find_first',
|
|
'make_dict', 'unwrap_from_path', 'read_only',
|
|
'read_write', 'getowner', 'set_contexts'])
|
|
|
|
|
|
def valid_name(name):
|
|
"""an option's name is a str and does not start with 'impl' or 'cfgimpl'
|
|
and name is not a function name"""
|
|
if not isinstance(name, str):
|
|
return False
|
|
return re.match(NAME_REGEXP, name) is not None and \
|
|
name not in FORBIDDEN_NAMES and \
|
|
not name.startswith('impl_') and \
|
|
not name.startswith('cfgimpl_')
|
|
|
|
|
|
def validate_callback(callback, callback_params, type_, callbackoption):
|
|
"""validate function and parameter set for callback, validation, ...
|
|
"""
|
|
def _validate_option(option):
|
|
#validate option
|
|
if hasattr(option, '_is_symlinkoption'):
|
|
if option._is_symlinkoption():
|
|
cur_opt = option._impl_getopt()
|
|
else:
|
|
cur_opt = option
|
|
else:
|
|
raise ValueError(_('{}_params must have an option'
|
|
' not a {} for first argument'
|
|
).format(type_, type(option)))
|
|
if cur_opt != callbackoption:
|
|
cur_opt._add_dependencies(callbackoption)
|
|
|
|
def _validate_force_permissive(force_permissive):
|
|
#validate force_permissive
|
|
if not isinstance(force_permissive, bool):
|
|
raise ValueError(_('{}_params must have a boolean'
|
|
' not a {} for second argument'
|
|
).format(type_, type(
|
|
force_permissive)))
|
|
|
|
def _validate_callback(callbk):
|
|
if isinstance(callbk, tuple):
|
|
if len(callbk) == 1:
|
|
if callbk not in ((None,), ('index',)):
|
|
raise ValueError(_('{0}_params with length of '
|
|
'tuple as 1 must only have '
|
|
'None as first value').format(type_))
|
|
return
|
|
elif len(callbk) != 2:
|
|
raise ValueError(_('{0}_params must only have 1 or 2 '
|
|
'as length').format(type_))
|
|
option, force_permissive = callbk
|
|
_validate_option(option)
|
|
_validate_force_permissive(force_permissive)
|
|
|
|
if not isinstance(callback, FunctionType):
|
|
raise ValueError(_('{0} must be a function').format(type_))
|
|
if callback_params is not None:
|
|
if not isinstance(callback_params, dict):
|
|
raise ValueError(_('{0}_params must be a dict').format(type_))
|
|
for key, callbacks in callback_params.items():
|
|
if key != '' and len(callbacks) != 1:
|
|
raise ValueError(_("{0}_params with key {1} mustn't have "
|
|
"length different to 1").format(type_,
|
|
key))
|
|
if not isinstance(callbacks, tuple):
|
|
raise ValueError(_('{0}_params must be tuple for key "{1}"'
|
|
).format(type_, key))
|
|
for callbk in callbacks:
|
|
_validate_callback(callbk)
|
|
|
|
|
|
#____________________________________________________________
|
|
#
|
|
class Base(object):
|
|
"""Base use by all *Option* classes (Option, OptionDescription, SymLinkOption, ...)
|
|
"""
|
|
__slots__ = ('_name',
|
|
'_informations',
|
|
#calcul
|
|
'_subdyn',
|
|
'_requires',
|
|
'_properties',
|
|
'_calc_properties',
|
|
#
|
|
'_consistencies',
|
|
#other
|
|
'_has_dependency',
|
|
'_dependencies',
|
|
'__weakref__'
|
|
)
|
|
|
|
def __init__(self, name, doc, requires=None, properties=None, is_multi=False):
|
|
if not valid_name(name):
|
|
raise ValueError(_("invalid name: {0} for option").format(name))
|
|
if requires is not None:
|
|
calc_properties, requires = validate_requires_arg(self, is_multi,
|
|
requires, name)
|
|
else:
|
|
calc_properties = frozenset()
|
|
requires = undefined
|
|
if properties is None:
|
|
properties = tuple()
|
|
if not isinstance(properties, tuple):
|
|
raise TypeError(_('invalid properties type {0} for {1},'
|
|
' must be a tuple').format(
|
|
type(properties),
|
|
name))
|
|
if calc_properties != frozenset([]) and properties is not tuple():
|
|
set_forbidden_properties = calc_properties & set(properties)
|
|
if set_forbidden_properties != frozenset():
|
|
raise ValueError('conflict: properties already set in '
|
|
'requirement {0}'.format(
|
|
list(set_forbidden_properties)))
|
|
_setattr = object.__setattr__
|
|
_setattr(self, '_name', name)
|
|
if sys.version_info[0] < 3 and isinstance(doc, str):
|
|
doc = doc.decode('utf8')
|
|
_setattr(self, '_informations', {'doc': doc})
|
|
if calc_properties is not undefined:
|
|
_setattr(self, '_calc_properties', calc_properties)
|
|
if requires is not undefined:
|
|
_setattr(self, '_requires', requires)
|
|
if properties is not undefined:
|
|
_setattr(self, '_properties', properties)
|
|
|
|
def _build_validator_params(self, validator, validator_params):
|
|
if sys.version_info[0] < 3:
|
|
func_args = getargspec(validator)
|
|
defaults = func_args.defaults
|
|
if defaults is None:
|
|
defaults = []
|
|
args = func_args.args[0:len(func_args.args)-len(defaults)]
|
|
else: # pragma: no cover
|
|
func_params = signature(validator).parameters
|
|
args = [f.name for f in func_params.values() if f.default is f.empty]
|
|
if validator_params is not None:
|
|
kwargs = list(validator_params.keys())
|
|
if '' in kwargs:
|
|
kwargs.remove('')
|
|
for kwarg in kwargs:
|
|
if kwarg in args:
|
|
args = args[0:args.index(kwarg)]
|
|
len_args = len(validator_params.get('', []))
|
|
if len_args != 0 and len(args) >= len_args:
|
|
args = args[0:len(args)-len_args]
|
|
if len(args) >= 2:
|
|
if validator_params is not None and '' in validator_params:
|
|
params = list(validator_params[''])
|
|
params.append((self, False))
|
|
validator_params[''] = tuple(params)
|
|
else:
|
|
if validator_params is None:
|
|
validator_params = {}
|
|
validator_params[''] = ((self, False),)
|
|
if len(args) == 3 and args[2] not in validator_params:
|
|
params = list(validator_params[''])
|
|
params.append(('index',))
|
|
validator_params[''] = tuple(params)
|
|
return validator_params
|
|
|
|
def _set_has_dependency(self):
|
|
if not self._is_symlinkoption():
|
|
self._has_dependency = True
|
|
|
|
def impl_has_dependency(self):
|
|
return getattr(self, '_has_dependency', False)
|
|
|
|
def impl_set_callback(self, callback, callback_params=None, _init=False):
|
|
if callback is None and callback_params is not None:
|
|
raise ValueError(_("params defined for a callback function but "
|
|
"no callback defined"
|
|
" yet for option {0}").format(
|
|
self.impl_getname()))
|
|
if not _init and self.impl_get_callback()[0] is not None:
|
|
raise ConfigError(_("a callback is already set for {0}, "
|
|
"cannot set another one's").format(self.impl_getname()))
|
|
self._validate_callback(callback, callback_params)
|
|
if callback is not None:
|
|
validate_callback(callback, callback_params, 'callback', self)
|
|
val = getattr(self, '_val_call', (None,))[0]
|
|
if callback_params is None or callback_params == {}:
|
|
val_call = (callback,)
|
|
else:
|
|
val_call = tuple([callback, callback_params])
|
|
self._val_call = (val, val_call)
|
|
|
|
def impl_is_optiondescription(self):
|
|
return self.__class__.__name__ in ['OptionDescription',
|
|
'DynOptionDescription',
|
|
'SynDynOptionDescription']
|
|
|
|
def impl_is_dynoptiondescription(self):
|
|
return self.__class__.__name__ in ['DynOptionDescription',
|
|
'SynDynOptionDescription']
|
|
|
|
def impl_getname(self):
|
|
return self._name
|
|
|
|
def impl_is_readonly(self):
|
|
return not isinstance(getattr(self, '_informations', dict()), dict)
|
|
|
|
def impl_getproperties(self):
|
|
return self._properties
|
|
|
|
def _set_readonly(self, has_extra):
|
|
if not self.impl_is_readonly():
|
|
_setattr = object.__setattr__
|
|
dico = self._informations
|
|
keys = tuple(dico.keys())
|
|
if len(keys) == 1:
|
|
dico = dico['doc']
|
|
else:
|
|
dico = tuple([keys, tuple(dico.values())])
|
|
_setattr(self, '_informations', dico)
|
|
if has_extra:
|
|
extra = getattr(self, '_extra', None)
|
|
if extra is not None:
|
|
_setattr(self, '_extra', tuple([tuple(extra.keys()), tuple(extra.values())]))
|
|
|
|
def _impl_setsubdyn(self, subdyn):
|
|
self._subdyn = subdyn
|
|
|
|
def impl_getrequires(self):
|
|
return getattr(self, '_requires', STATIC_TUPLE)
|
|
|
|
def impl_get_callback(self):
|
|
call = getattr(self, '_val_call', (None, None))[1]
|
|
if call is None:
|
|
ret_call = (None, {})
|
|
elif len(call) == 1:
|
|
ret_call = (call[0], {})
|
|
else:
|
|
ret_call = call
|
|
return ret_call
|
|
|
|
# ____________________________________________________________
|
|
# information
|
|
def impl_get_information(self, key, default=undefined):
|
|
"""retrieves one information's item
|
|
|
|
:param key: the item string (ex: "help")
|
|
"""
|
|
def _is_string(infos):
|
|
if sys.version_info[0] >= 3: # pragma: no cover
|
|
return isinstance(infos, str)
|
|
else:
|
|
return isinstance(infos, str) or isinstance(infos, unicode)
|
|
|
|
dico = self._informations
|
|
if isinstance(dico, tuple):
|
|
if key in dico[0]:
|
|
return dico[1][dico[0].index(key)]
|
|
elif _is_string(dico):
|
|
if key == 'doc':
|
|
return dico
|
|
elif isinstance(dico, dict):
|
|
if key in dico:
|
|
return dico[key]
|
|
if default is not undefined:
|
|
return default
|
|
raise ValueError(_("information's item not found: {0}").format(
|
|
key))
|
|
|
|
def impl_set_information(self, key, value):
|
|
"""updates the information's attribute
|
|
(which is a dictionary)
|
|
|
|
:param key: information's key (ex: "help", "doc"
|
|
:param value: information's value (ex: "the help string")
|
|
"""
|
|
if self.impl_is_readonly():
|
|
raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
|
|
" read-only").format(
|
|
self.__class__.__name__,
|
|
self,
|
|
#self.impl_getname(),
|
|
key))
|
|
self._informations[key] = value
|
|
|
|
|
|
class BaseOption(Base):
|
|
"""This abstract base class stands for attribute access
|
|
in options that have to be set only once, it is of course done in the
|
|
__setattr__ method
|
|
"""
|
|
__slots__ = tuple()
|
|
|
|
def __getstate__(self):
|
|
raise NotImplementedError()
|
|
|
|
def __setattr__(self, name, value):
|
|
"""set once and only once some attributes in the option,
|
|
like `_name`. `_name` cannot be changed one the option and
|
|
pushed in the :class:`tiramisu.option.OptionDescription`.
|
|
|
|
if the attribute `_readonly` is set to `True`, the option is
|
|
"frozen" (which has noting to do with the high level "freeze"
|
|
propertie or "read_only" property)
|
|
"""
|
|
if name != '_option' and \
|
|
not isinstance(value, tuple):
|
|
is_readonly = False
|
|
# never change _name dans _opt
|
|
if name == '_name':
|
|
if self.impl_getname() is not None:
|
|
#so _name is already set
|
|
is_readonly = True
|
|
elif name != '_readonly':
|
|
is_readonly = self.impl_is_readonly()
|
|
if is_readonly:
|
|
raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
|
|
" read-only").format(
|
|
self.__class__.__name__,
|
|
self,
|
|
#self.impl_getname(),
|
|
name))
|
|
super(BaseOption, self).__setattr__(name, value)
|
|
|
|
def impl_getpath(self, context):
|
|
return context.cfgimpl_get_description().impl_get_path_by_opt(self)
|
|
|
|
def impl_has_callback(self):
|
|
"to know if a callback has been defined or not"
|
|
return self.impl_get_callback()[0] is not None
|
|
|
|
def _is_subdyn(self):
|
|
return getattr(self, '_subdyn', None) is not None
|
|
|
|
def _impl_valid_unicode(self, value):
|
|
if sys.version_info[0] >= 3: # pragma: no cover
|
|
if not isinstance(value, str):
|
|
return ValueError(_('invalid string'))
|
|
else:
|
|
if not isinstance(value, unicode) and not isinstance(value, str):
|
|
return ValueError(_('invalid unicode or string'))
|
|
|
|
def impl_get_display_name(self, dyn_name=None):
|
|
name = self.impl_getdoc()
|
|
if name is None or name == '':
|
|
if dyn_name is not None:
|
|
name = dyn_name
|
|
else:
|
|
name = self.impl_getname()
|
|
if sys.version_info[0] < 3 and isinstance(name, unicode):
|
|
name = name.encode('utf8')
|
|
return name
|
|
|
|
def reset_cache(self, opt, obj, type_):
|
|
context = obj._getcontext()
|
|
path = self.impl_getpath(context)
|
|
obj._p_.delcache(path)
|
|
context.cfgimpl_reset_cache(only=(type_,),
|
|
opt=self,
|
|
path=path)
|
|
|
|
def _is_symlinkoption(self):
|
|
return False
|
|
|
|
|
|
class OnlyOption(BaseOption):
|
|
__slots__ = tuple()
|
|
|
|
|
|
def validate_requires_arg(new_option, multi, requires, name):
|
|
"""check malformed requirements
|
|
and tranform dict to internal tuple
|
|
|
|
:param requires: have a look at the
|
|
:meth:`tiramisu.setting.Settings.apply_requires` method to
|
|
know more about
|
|
the description of the requires dictionary
|
|
"""
|
|
def set_dependency(option):
|
|
if not getattr(option, '_dependencies', None):
|
|
options = set()
|
|
else:
|
|
options = set(option._dependencies)
|
|
options.add(new_option)
|
|
option._dependencies = tuple(options)
|
|
|
|
def get_option(require):
|
|
option = require['option']
|
|
if not hasattr(option, '_is_symlinkoption'):
|
|
raise ValueError(_('malformed requirements '
|
|
'must be an option in option {0}').format(name))
|
|
if not multi and option.impl_is_multi():
|
|
raise ValueError(_('malformed requirements '
|
|
'multi option must not set '
|
|
'as requires of non multi option {0}').format(name))
|
|
set_dependency(option)
|
|
return option
|
|
|
|
def _set_expected(action, inverse, transitive, same_action, option, expected, operator):
|
|
if inverse not in ret_requires[action]:
|
|
ret_requires[action][inverse] = ([(option, [expected])], action, inverse, transitive, same_action, operator)
|
|
else:
|
|
for exp in ret_requires[action][inverse][0]:
|
|
if exp[0] == option:
|
|
exp[1].append(expected)
|
|
break
|
|
else:
|
|
ret_requires[action][inverse][0].append((option, [expected]))
|
|
|
|
def set_expected(require, ret_requires):
|
|
expected = require['expected']
|
|
inverse = get_inverse(require)
|
|
transitive = get_transitive(require)
|
|
same_action = get_sameaction(require)
|
|
operator = get_operator(require)
|
|
if isinstance(expected, list):
|
|
for exp in expected:
|
|
if set(exp.keys()) != {'option', 'value'}:
|
|
raise ValueError(_('malformed requirements expected must have '
|
|
'option and value for option {0}').format(name))
|
|
option = exp['option']
|
|
set_dependency(option)
|
|
if option is not None:
|
|
err = option._validate(exp['value'])
|
|
if err:
|
|
raise ValueError(_('malformed requirements expected value '
|
|
'must be valid for option {0}'
|
|
': {1}').format(name, err))
|
|
_set_expected(action, inverse, transitive, same_action, option, exp['value'], operator)
|
|
else:
|
|
option = get_option(require)
|
|
if expected is not None:
|
|
err = option._validate(expected)
|
|
if err:
|
|
raise ValueError(_('malformed requirements expected value '
|
|
'must be valid for option {0}'
|
|
': {1}').format(name, err))
|
|
_set_expected(action, inverse, transitive, same_action, option, expected, operator)
|
|
|
|
def get_action(require):
|
|
action = require['action']
|
|
if action == 'force_store_value':
|
|
raise ValueError(_("malformed requirements for option: {0}"
|
|
" action cannot be force_store_value"
|
|
).format(name))
|
|
return action
|
|
|
|
def get_inverse(require):
|
|
inverse = require.get('inverse', False)
|
|
if inverse not in [True, False]:
|
|
raise ValueError(_('malformed requirements for option: {0}'
|
|
' inverse must be boolean'))
|
|
return inverse
|
|
|
|
def get_transitive(require):
|
|
transitive = require.get('transitive', True)
|
|
if transitive not in [True, False]:
|
|
raise ValueError(_('malformed requirements for option: {0}'
|
|
' transitive must be boolean'))
|
|
return transitive
|
|
|
|
def get_sameaction(require):
|
|
same_action = require.get('same_action', True)
|
|
if same_action not in [True, False]:
|
|
raise ValueError(_('malformed requirements for option: {0}'
|
|
' same_action must be boolean'))
|
|
return same_action
|
|
|
|
def get_operator(require):
|
|
operator = require.get('operator', 'or')
|
|
if operator not in ['and', 'or']:
|
|
raise ValueError(_('malformed requirements for option: "{0}"'
|
|
' operator must be "or" or "and"').format(operator))
|
|
return operator
|
|
|
|
|
|
ret_requires = {}
|
|
config_action = set()
|
|
|
|
# start parsing all requires given by user (has dict)
|
|
# transforme it to a tuple
|
|
for require in requires:
|
|
if not isinstance(require, dict):
|
|
raise ValueError(_("malformed requirements type for option:"
|
|
" {0}, must be a dict").format(name))
|
|
valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
|
|
'same_action', 'operator')
|
|
unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
|
|
if unknown_keys != frozenset():
|
|
raise ValueError(_('malformed requirements for option: {0}'
|
|
' unknown keys {1}, must only '
|
|
'{2}').format(name,
|
|
unknown_keys,
|
|
valid_keys))
|
|
# prepare all attributes
|
|
if not ('expected' in require and isinstance(require['expected'], list)) and \
|
|
not ('option' in require and 'expected' in require) or \
|
|
'action' not in require:
|
|
raise ValueError(_("malformed requirements for option: {0}"
|
|
" require must have option, expected and"
|
|
" action keys").format(name))
|
|
action = get_action(require)
|
|
config_action.add(action)
|
|
if action not in ret_requires:
|
|
ret_requires[action] = {}
|
|
set_expected(require, ret_requires)
|
|
|
|
# transform dict to tuple
|
|
ret = []
|
|
for requires in ret_requires.values():
|
|
ret_action = []
|
|
for require in requires.values():
|
|
ret_action.append((tuple(require[0]), require[1],
|
|
require[2], require[3], require[4], require[5]))
|
|
ret.append(tuple(ret_action))
|
|
return frozenset(config_action), tuple(ret)
|
|
|
|
|
|
class SymLinkOption(OnlyOption):
|
|
|
|
def __init__(self, name, opt):
|
|
if not isinstance(opt, OnlyOption) or \
|
|
opt._is_symlinkoption():
|
|
raise ValueError(_('malformed symlinkoption '
|
|
'must be an option '
|
|
'for symlink {0}').format(name))
|
|
_setattr = object.__setattr__
|
|
_setattr(self, '_name', name)
|
|
_setattr(self, '_opt', opt)
|
|
opt._set_has_dependency()
|
|
|
|
def _is_symlinkoption(self):
|
|
return True
|
|
|
|
def __getattr__(self, name, context=undefined):
|
|
return getattr(self._impl_getopt(), name)
|
|
|
|
def _impl_getopt(self):
|
|
return self._opt
|
|
|
|
def impl_get_information(self, key, default=undefined):
|
|
return self._impl_getopt().impl_get_information(key, default)
|
|
|
|
def impl_is_readonly(self):
|
|
return True
|
|
|
|
def impl_getproperties(self):
|
|
return self._impl_getopt()._properties
|
|
|
|
def impl_get_callback(self):
|
|
return self._impl_getopt().impl_get_callback()
|
|
|
|
def impl_has_callback(self):
|
|
"to know if a callback has been defined or not"
|
|
return self._impl_getopt().impl_has_callback()
|
|
|
|
def impl_is_multi(self):
|
|
return self._impl_getopt().impl_is_multi()
|
|
|
|
def _is_subdyn(self):
|
|
return getattr(self._impl_getopt(), '_subdyn', None) is not None
|
|
|
|
def _get_consistencies(self):
|
|
return ()
|
|
|
|
def _has_consistencies(self):
|
|
return False
|
|
|
|
|
|
class DynSymLinkOption(object):
|
|
__slots__ = ('_dyn', '_opt', '_name')
|
|
|
|
def __init__(self, name, opt, dyn):
|
|
self._name = name
|
|
self._dyn = dyn
|
|
self._opt = opt
|
|
|
|
def __getattr__(self, name, context=undefined):
|
|
return getattr(self._impl_getopt(), name)
|
|
|
|
def impl_getname(self):
|
|
return self._name
|
|
|
|
def impl_get_display_name(self):
|
|
return self._impl_getopt().impl_get_display_name(dyn_name=self.impl_getname())
|
|
|
|
def _impl_getopt(self):
|
|
return self._opt
|
|
|
|
def impl_getsuffix(self):
|
|
return self._dyn.split('.')[-1][len(self._impl_getopt().impl_getname()):]
|
|
|
|
def impl_getpath(self, context):
|
|
path = self._impl_getopt().impl_getpath(context)
|
|
base_path = '.'.join(path.split('.')[:-2])
|
|
if self.impl_is_master_slaves() and base_path is not '':
|
|
base_path = base_path + self.impl_getsuffix()
|
|
if base_path == '':
|
|
return self._dyn
|
|
else:
|
|
return base_path + '.' + self._dyn
|
|
|
|
def impl_validate(self, value, context=undefined, validate=True,
|
|
force_index=None, force_submulti_index=None, is_multi=None,
|
|
display_error=True, display_warnings=True, multi=None,
|
|
setting_properties=undefined):
|
|
return self._impl_getopt().impl_validate(value, context, validate,
|
|
force_index,
|
|
force_submulti_index,
|
|
current_opt=self,
|
|
is_multi=is_multi,
|
|
display_error=display_error,
|
|
display_warnings=display_warnings,
|
|
multi=multi,
|
|
setting_properties=setting_properties)
|
|
|
|
def impl_is_dynsymlinkoption(self):
|
|
return True
|