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
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from .__version__ import __version__
try:
from .convert import Rougail
from .config import RougailConfig
__all__ = ("Rougail", "RougailConfig", "__version__")
except ModuleNotFoundError as err:
__all__ = ("__version__",)

View file

@ -162,6 +162,16 @@ class Annotator(Walk): # pylint: disable=R0903
)
if calculated_variable is not 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)
variable.multi = calc_multi_for_type_variable(
variable,

View file

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

View file

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

View file

@ -28,7 +28,12 @@ from pydantic import (
)
import tiramisu
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 ..error import DictConsistencyError, VariableCalculationDependencyError
from ..tiramisu import CONVERT_OPTION

View file

@ -35,6 +35,7 @@ from importlib.util import (
from unicodedata import normalize, combining
from jinja2 import StrictUndefined, DictLoader
from jinja2.sandbox import SandboxedEnvironment
from re import findall
from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error
from tiramisu.error import (
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):
try:
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]),
)
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 re import findall
from tiramisu import Calculation, owners
from tiramisu.error import (
@ -31,7 +30,11 @@ from tiramisu.error import (
CancelParam,
)
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 .i18n import _
@ -41,13 +44,14 @@ class UserDatas:
def __init__(self, config) -> None:
self.config = config
def user_datas(self,
user_datas: List[dict],
*,
return_values_not_error=False,
user_datas_type: str="user_datas",
only_default: bool=False,
):
def user_datas(
self,
user_datas: List[dict],
*,
return_values_not_error=False,
user_datas_type: str = "user_datas",
only_default: bool = False,
):
self.values = {}
self.errors = []
self.warnings = []
@ -126,7 +130,9 @@ class UserDatas:
if not tconfig.isdynamic(only_self=True):
# it's not a dynamic variable
continue
identifier = self._get_identifier(tconfig.name(), name)
identifier = get_identifier_from_dynamic_family(
tconfig.name(), name
)
if identifier != normalize_family(identifier):
msg = _(
'cannot load variable path "{0}", the identifier "{1}" is not valid in {2}'
@ -240,7 +246,8 @@ class UserDatas:
else:
if "source" in self.values[path]:
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
if index is not None:
@ -256,15 +263,6 @@ class UserDatas:
if not value_is_set:
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):
if not self.show_secrets and option.type() == "password":
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_ok = ['04_5validators_multi3']
#test_ok = []
# test_raise = ['22_0calculation_variable_leader_follower_multi']
# test_raise = ['80unknown_default_variable_inside_dynamic_family']
#test_raise = []
test_multi = True
#test_multi = False
@ -181,7 +181,7 @@ def test_dictionary_namespace(test_dir):
assert getcwd() == ORI_DIR
def test_error_dictionary(test_dir_error):
def test_error_dictionary_namespace(test_dir_error):
assert getcwd() == ORI_DIR
test_dir_ = join(errors_dirs, test_dir_error)
errno = []
@ -202,3 +202,28 @@ def test_error_dictionary(test_dir_error):
if isdir(tiramisu_tmp_dir):
rmtree(tiramisu_tmp_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.error import DictConsistencyError
from tiramisu.error import ConflictError
from rougail_tests.utils import config_to_dict
@ -95,3 +96,74 @@ def test_namespace():
'ns2.var2': 'NS2',
'ns3.var1': '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()