feat(40): better conflict error message with dynamic name

This commit is contained in:
egarette@silique.fr 2025-09-20 18:46:08 +02:00
parent c8d5656094
commit 9ee03b22bb
21 changed files with 244 additions and 26 deletions

View file

@ -24,10 +24,13 @@ details.
You should have received a copy of the GNU Lesser General Public License 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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from .__version__ import __version__ from .__version__ import __version__
try: try:
from .convert import Rougail from .convert import Rougail
from .config import RougailConfig from .config import RougailConfig
__all__ = ("Rougail", "RougailConfig", "__version__") __all__ = ("Rougail", "RougailConfig", "__version__")
except ModuleNotFoundError as err: except ModuleNotFoundError as err:
__all__ = ("__version__",) __all__ = ("__version__",)

View file

@ -162,6 +162,16 @@ class Annotator(Walk): # pylint: disable=R0903
) )
if calculated_variable is not None: if calculated_variable is not None:
if calculated_variable.multi is None: if calculated_variable.multi is None:
if (
isinstance(calculated_variable.default, VariableCalculation)
and variable.path == calculated_variable.default.path
):
msg = _(
'the "{0}" default value is a calculation with itself'.format(
variable.path
)
)
raise DictConsistencyError(msg, 75, variable.xmlfiles)
self._convert_variable_multi(calculated_variable) self._convert_variable_multi(calculated_variable)
variable.multi = calc_multi_for_type_variable( variable.multi = calc_multi_for_type_variable(
variable, variable,

View file

@ -34,7 +34,6 @@ from ..tiramisu import normalize_family
from ..convert import RougailConvert from ..convert import RougailConvert
from ..convert.object_model import get_convert_option_types from ..convert.object_model import get_convert_option_types
#import rougail.structural_commandline.object_model
RENAMED = { RENAMED = {
"dictionaries_dir": "main_dictionaries", "dictionaries_dir": "main_dictionaries",
@ -179,7 +178,7 @@ class _RougailConfig:
yield f"{option.path()}: {option.value.get()}" yield f"{option.path()}: {option.value.get()}"
def __repr__(self): def __repr__(self):
print(self.config) self.generate_config()
self.config.property.read_write() self.config.property.read_write()
try: try:
values = "\n".join(self.parse(self.config)) values = "\n".join(self.parse(self.config))

View file

@ -1164,4 +1164,3 @@ class RougailConvert(ParserVariable):
tiramisu.write(output) tiramisu.write(output)
# print(output) # print(output)
return output return output

View file

@ -28,7 +28,12 @@ from pydantic import (
) )
import tiramisu import tiramisu
from tiramisu.config import get_common_path from tiramisu.config import get_common_path
from ..utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined, PROPERTY_ATTRIBUTE from ..utils import (
get_jinja_variable_to_param,
calc_multi_for_type_variable,
undefined,
PROPERTY_ATTRIBUTE,
)
from ..i18n import _ from ..i18n import _
from ..error import DictConsistencyError, VariableCalculationDependencyError from ..error import DictConsistencyError, VariableCalculationDependencyError
from ..tiramisu import CONVERT_OPTION from ..tiramisu import CONVERT_OPTION

View file

@ -35,6 +35,7 @@ from importlib.util import (
from unicodedata import normalize, combining from unicodedata import normalize, combining
from jinja2 import StrictUndefined, DictLoader from jinja2 import StrictUndefined, DictLoader
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
from re import findall
from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error
from tiramisu.error import ( from tiramisu.error import (
ValueWarning, ValueWarning,
@ -134,6 +135,16 @@ CONVERT_OPTION = {
} }
def get_identifier_from_dynamic_family(true_name, name) -> str:
if true_name == "{{ identifier }}":
return name
regexp = true_name.replace("{{ identifier }}", "(.*)")
finded = findall(regexp, name)
if len(finded) != 1 or not finded[0]:
return None
return finded[0]
def raise_carry_out_calculation_error(subconfig, *args, **kwargs): def raise_carry_out_calculation_error(subconfig, *args, **kwargs):
try: try:
ori_raise_carry_out_calculation_error(subconfig, *args, **kwargs) ori_raise_carry_out_calculation_error(subconfig, *args, **kwargs)
@ -423,3 +434,11 @@ class ConvertDynOptionDescription(DynOptionDescription):
self.convert_identifier_to_path(self.get_identifiers(subconfig)[-1]), self.convert_identifier_to_path(self.get_identifiers(subconfig)[-1]),
) )
return display return display
def name_could_conflict(self, dynchild, child):
return (
get_identifier_from_dynamic_family(
dynchild.impl_getname(), child.impl_getname()
)
is not None
)

View file

@ -20,7 +20,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
""" """
from typing import List from typing import List
from re import findall
from tiramisu import Calculation, owners from tiramisu import Calculation, owners
from tiramisu.error import ( from tiramisu.error import (
@ -31,7 +30,11 @@ from tiramisu.error import (
CancelParam, CancelParam,
) )
from .utils import undefined from .utils import undefined
from .tiramisu import normalize_family, CONVERT_OPTION from .tiramisu import (
normalize_family,
CONVERT_OPTION,
get_identifier_from_dynamic_family,
)
from .error import DictConsistencyError from .error import DictConsistencyError
from .i18n import _ from .i18n import _
@ -41,13 +44,14 @@ class UserDatas:
def __init__(self, config) -> None: def __init__(self, config) -> None:
self.config = config self.config = config
def user_datas(self, def user_datas(
user_datas: List[dict], self,
*, user_datas: List[dict],
return_values_not_error=False, *,
user_datas_type: str="user_datas", return_values_not_error=False,
only_default: bool=False, user_datas_type: str = "user_datas",
): only_default: bool = False,
):
self.values = {} self.values = {}
self.errors = [] self.errors = []
self.warnings = [] self.warnings = []
@ -126,7 +130,9 @@ class UserDatas:
if not tconfig.isdynamic(only_self=True): if not tconfig.isdynamic(only_self=True):
# it's not a dynamic variable # it's not a dynamic variable
continue continue
identifier = self._get_identifier(tconfig.name(), name) identifier = get_identifier_from_dynamic_family(
tconfig.name(), name
)
if identifier != normalize_family(identifier): if identifier != normalize_family(identifier):
msg = _( msg = _(
'cannot load variable path "{0}", the identifier "{1}" is not valid in {2}' 'cannot load variable path "{0}", the identifier "{1}" is not valid in {2}'
@ -240,7 +246,8 @@ class UserDatas:
else: else:
if "source" in self.values[path]: if "source" in self.values[path]:
option_without_index.information.set( option_without_index.information.set(
"loaded_from", _("loaded from {0}").format(self.values[path]["source"]) "loaded_from",
_("loaded from {0}").format(self.values[path]["source"]),
) )
# value is correctly set, remove variable to the set # value is correctly set, remove variable to the set
if index is not None: if index is not None:
@ -256,15 +263,6 @@ class UserDatas:
if not value_is_set: if not value_is_set:
break break
def _get_identifier(self, true_name, name) -> str:
if true_name == "{{ identifier }}":
return name
regexp = true_name.replace("{{ identifier }}", "(.*)")
finded = findall(regexp, name)
if len(finded) != 1 or not finded[0]:
return None
return finded[0]
def _display_value(self, option, value): def _display_value(self, option, value):
if not self.show_secrets and option.type() == "password": if not self.show_secrets and option.type() == "password":
return "*" * 10 return "*" * 10

View file

@ -0,0 +1,19 @@
from tiramisu import *
from tiramisu.setting import ALLOWED_LEADER_PROPERTIES
from re import compile as re_compile
from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription
load_functions('../rougail-tests/funcs/test.py')
try:
groups.namespace
except:
groups.addgroup('namespace')
ALLOWED_LEADER_PROPERTIES.add("basic")
ALLOWED_LEADER_PROPERTIES.add("standard")
ALLOWED_LEADER_PROPERTIES.add("advanced")
option_4 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml'], 'type': 'string'})
optiondescription_3 = OptionDescription(name="https_proxy", doc="{{ identifier }} Proxy", children=[option_4], properties=frozenset({"basic"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml']})
option_6 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml'], 'type': 'string'})
optiondescription_5 = ConvertDynOptionDescription(name="{{ identifier }}_proxy", doc="{{ identifier }} Proxy", identifiers=["HTTPS", "SOCKS"], children=[option_6], properties=frozenset({"basic"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml']})
optiondescription_2 = OptionDescription(name="manual", doc="manual", children=[optiondescription_3, optiondescription_5], properties=frozenset({"basic"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml']})
optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[optiondescription_2], properties=frozenset({"basic"}), informations={'ymlfiles': ['']})
option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1])

View file

@ -0,0 +1,19 @@
from tiramisu import *
from tiramisu.setting import ALLOWED_LEADER_PROPERTIES
from re import compile as re_compile
from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription
load_functions('../rougail-tests/funcs/test.py')
try:
groups.namespace
except:
groups.addgroup('namespace')
ALLOWED_LEADER_PROPERTIES.add("basic")
ALLOWED_LEADER_PROPERTIES.add("standard")
ALLOWED_LEADER_PROPERTIES.add("advanced")
option_4 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml'], 'type': 'string'})
optiondescription_3 = OptionDescription(name="https_proxy", doc="{{ identifier }} Proxy", children=[option_4], properties=frozenset({"basic"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml']})
option_6 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml'], 'type': 'string'})
optiondescription_5 = ConvertDynOptionDescription(name="{{ identifier }}_proxy", doc="{{ identifier }} Proxy", identifiers=["HTTPS", "SOCKS"], children=[option_6], properties=frozenset({"basic"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml']})
optiondescription_2 = OptionDescription(name="manual", doc="manual", children=[optiondescription_3, optiondescription_5], properties=frozenset({"basic"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml']})
optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[optiondescription_2], properties=frozenset({"basic"}), informations={'ymlfiles': ['']})
option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1])

View file

@ -0,0 +1,25 @@
---
version: 1.1
manual:
use_for_https:
description: Also use this proxy for HTTPS
default: true
"{{ identifier }}_proxy":
description: "{{ identifier }} Proxy"
dynamic:
- HTTPS
- SOCKS
hidden:
variable: rougail.manual.use_for_https
address:
description: "{{ identifier }} address"
default:
variable: rougail.manual.http_proxy.address
port:
description: "{{ identifier }} port"
default:
variable: rougail.manual.http_proxy.port

View file

@ -0,0 +1,25 @@
---
version: 1.1
manual:
use_for_https:
description: Also use this proxy for HTTPS
default: true
"{{ identifier }}_proxy":
description: "{{ identifier }} Proxy"
dynamic:
- HTTPS
- SOCKS
hidden:
variable: rougail.manual.use_for_https
address:
description: "{{ identifier }} address"
default:
variable: rougail.manual.http_proxy.address
port:
description: "{{ identifier }} port"
default:
variable: rougail.manual.http_proxy.port

View file

@ -49,7 +49,7 @@ test_ok -= excludes
test_raise -= excludes test_raise -= excludes
# test_ok = ['04_5validators_multi3'] # test_ok = ['04_5validators_multi3']
#test_ok = [] #test_ok = []
# test_raise = ['22_0calculation_variable_leader_follower_multi'] # test_raise = ['80unknown_default_variable_inside_dynamic_family']
#test_raise = [] #test_raise = []
test_multi = True test_multi = True
#test_multi = False #test_multi = False
@ -181,7 +181,7 @@ def test_dictionary_namespace(test_dir):
assert getcwd() == ORI_DIR assert getcwd() == ORI_DIR
def test_error_dictionary(test_dir_error): def test_error_dictionary_namespace(test_dir_error):
assert getcwd() == ORI_DIR assert getcwd() == ORI_DIR
test_dir_ = join(errors_dirs, test_dir_error) test_dir_ = join(errors_dirs, test_dir_error)
errno = [] errno = []
@ -202,3 +202,28 @@ def test_error_dictionary(test_dir_error):
if isdir(tiramisu_tmp_dir): if isdir(tiramisu_tmp_dir):
rmtree(tiramisu_tmp_dir) rmtree(tiramisu_tmp_dir)
assert getcwd() == ORI_DIR assert getcwd() == ORI_DIR
def test_error_dictionary(test_dir_error):
assert getcwd() == ORI_DIR
test_dir_ = join(errors_dirs, test_dir_error)
if isfile(join(test_dir_, 'force_namespace')):
return
errno = []
rougailconfig = RougailConfig.copy()
rougailconfig['main_namespace'] = None
eolobj = load_rougail_object(test_dir_, rougailconfig, namespace=False)
if eolobj is None:
return
for i in listdir(test_dir_):
if i.startswith('errno_'):
errno.append(int(i.split('_')[1]))
if not errno:
errno.append(0)
with raises(DictConsistencyError) as err:
save(test_dir_, eolobj, error=True)
assert err.value.errno in errno, f'expected errno: {errno}, errno: {err.value.errno}, msg: {err}'
tiramisu_tmp_dir = dirname(get_tiramisu_filename(test_dir_, 'tmp', False, True))
if isdir(tiramisu_tmp_dir):
rmtree(tiramisu_tmp_dir)
assert getcwd() == ORI_DIR

View file

@ -3,6 +3,7 @@ import logging
from rougail import Rougail, RougailConfig from rougail import Rougail, RougailConfig
from rougail.error import DictConsistencyError from rougail.error import DictConsistencyError
from tiramisu.error import ConflictError
from rougail_tests.utils import config_to_dict from rougail_tests.utils import config_to_dict
@ -95,3 +96,74 @@ def test_namespace():
'ns2.var2': 'NS2', 'ns2.var2': 'NS2',
'ns3.var1': 'NS3', 'ns3.var1': 'NS3',
'ns3.var2': 'NS3'} 'ns3.var2': 'NS3'}
#
#
#def test_duplicate_0():
# rougailconfig = RougailConfig.copy()
# rougailconfig['main_namespace'] = None
# rougailconfig['dictionaries_dir'] = ['tests/duplicates/0/']
# eolobj = Rougail(rougailconfig=rougailconfig)
# cfg = eolobj.run()
# cfg.value.get()
# cfg.option('od.od_val1').value.get()
# cfg.option('od.od_val2').value.get()
def test_duplicate_1():
rougailconfig = RougailConfig.copy()
rougailconfig['main_namespace'] = None
rougailconfig['dictionaries_dir'] = ['tests/duplicates/1/']
eolobj = Rougail(rougailconfig=rougailconfig)
cfg = eolobj.run()
with raises(ConflictError):
cfg.value.get()
with raises(ConflictError):
cfg.option('od.od_val').value.get()
def test_duplicate_2():
rougailconfig = RougailConfig.copy()
rougailconfig['main_namespace'] = None
rougailconfig['dictionaries_dir'] = ['tests/duplicates/2/']
eolobj = Rougail(rougailconfig=rougailconfig)
cfg = eolobj.run()
with raises(ConflictError):
cfg.value.get()
with raises(ConflictError):
cfg.option('od.od_val').value.get()
def test_duplicate_3():
rougailconfig = RougailConfig.copy()
rougailconfig['main_namespace'] = None
rougailconfig['dictionaries_dir'] = ['tests/duplicates/3/']
eolobj = Rougail(rougailconfig=rougailconfig)
cfg = eolobj.run()
with raises(ConflictError):
cfg.value.get()
with raises(ConflictError):
cfg.option('od.od_val').value.get()
#
#
#def test_duplicate_4():
# rougailconfig = RougailConfig.copy()
# rougailconfig['main_namespace'] = None
# rougailconfig['dictionaries_dir'] = ['tests/duplicates/4/']
# eolobj = Rougail(rougailconfig=rougailconfig)
# cfg = eolobj.run()
# with raises(ConflictError):
# cfg.value.get()
# with raises(ConflictError):
# cfg.option('od.od_val').value.get()
def test_duplicate_5():
rougailconfig = RougailConfig.copy()
rougailconfig['main_namespace'] = None
rougailconfig['dictionaries_dir'] = ['tests/duplicates/5/']
eolobj = Rougail(rougailconfig=rougailconfig)
cfg = eolobj.run()
with raises(ConflictError):
cfg.value.get()
with raises(ConflictError):
cfg.option('od.od_val').value.get()