From ba0b326b564e7cee9ae60140c1fa76942568d7db Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 2 Nov 2025 18:44:17 +0100 Subject: [PATCH] feat: support layers --- src/rougail/cli/__main__.py | 92 ++++++++++++------- src/rougail/cli/config.py | 8 ++ tests/cli/result_user_data.txt | 7 ++ tests/cli/result_user_datas.txt | 7 ++ tests/cli/result_user_datas_layers.txt | 7 ++ tests/cli/yaml/file.yml | 2 + tests/rougailcli_file/alt_console.txt | 2 +- .../structures_warnings/file.yml | 1 + tests/test_load.py | 75 +++++++++++++-- 9 files changed, 157 insertions(+), 44 deletions(-) create mode 100644 tests/cli/result_user_data.txt create mode 100644 tests/cli/result_user_datas.txt create mode 100644 tests/cli/result_user_datas_layers.txt create mode 100644 tests/cli/yaml/file.yml diff --git a/src/rougail/cli/__main__.py b/src/rougail/cli/__main__.py index f2fd300..b94a269 100644 --- a/src/rougail/cli/__main__.py +++ b/src/rougail/cli/__main__.py @@ -24,9 +24,10 @@ from warnings import warn from tiramisu_cmdline_parser import TiramisuCmdlineParser from tiramisu.error import PropertiesOptionError -from tiramisu import Config +from tiramisu import MetaConfig from rougail import Rougail +from rougail.user_datas import UserDatas from rougail.config import get_rougail_config from rougail.utils import load_modules from rougail.error import RougailWarning @@ -61,8 +62,8 @@ def _main(arguments, do_not_print): for version in versions: print(version) exit() - config, err_warn = load_user_datas(rougailconfig) - output = get_output(rougailconfig, config, err_warn) + metaconfig, config, err_warn = load_user_datas(rougailconfig) + output = get_output(rougailconfig, metaconfig, config, err_warn) if do_not_print: return output.run() ret = output.print() @@ -155,45 +156,68 @@ def manage_warnings(warnings): filterwarnings("default", category=DeprecationWarning) filterwarnings("default", category=RougailWarning) + def load_user_datas(rougailconfig): + if not rougailconfig["cli.load_config"]: + return None, {"errors": [], "warnings": []} try: user_data_names = rougailconfig["step.user_data"] except PropertiesOptionError: user_data_names = [] - # structural - if rougailconfig["cli.load_config"]: - rougail = Rougail(rougailconfig) - config = rougail.run() - config.property.read_write() + if rougailconfig["cli.layers"]: + layers = [[ud] for ud in user_data_names] + last_layers = len(layers) - 1 else: - config = None - # data user - user_datas = [] - for user_data_name in user_data_names: - path = ( - Path(__file__).parent.parent - / ("user_data_" + user_data_name) - / "__init__.py" - ) - if not path.is_file(): - raise Exception( - _('cannot find "user_data" module "{0}"').format(user_data_name) + layers = [user_data_names] + last_layers = 0 + rougail = Rougail(rougailconfig) + layer_name = "_".join(layers[-1]) + config = rougail.run(name=layer_name) + metaconfig = config + if last_layers: + for layer in layers[:-1]: + layer_name = "_".join(layer) + metaconfig = MetaConfig([metaconfig], name=layer_name) + metaconfig.owner.set(metaconfig.path()) + subconfig = metaconfig + err_warn = {"errors": [], "warnings": []} + for idx, layer in enumerate(layers): + if idx: + subconfig = subconfig.config("_".join(layer)) + config.owner.set(subconfig.path()) + subconfig.property.read_write() + # data user + user_datas = [] + for user_data_name in layer: + path = ( + Path(__file__).parent.parent + / ("user_data_" + user_data_name) + / "__init__.py" ) - module = load_modules("rougail.user_data_" + user_data_name, str(path)) - user_datas.extend( - module.RougailUserData( - config, - rougailconfig=rougailconfig, - ).run() - ) - if user_datas: - err_warn = rougail.user_datas(user_datas) - else: - err_warn = {"errors": [], "warnings": []} - return config, err_warn + if not path.is_file(): + raise Exception( + _('cannot find "user_data" module "{0}"').format(user_data_name) + ) + module = load_modules("rougail.user_data_" + user_data_name, str(path)) + user_datas.extend( + module.RougailUserData( + subconfig, + rougailconfig=rougailconfig, + ).run() + ) + if user_datas: + new_err_warn = UserDatas(subconfig).user_datas(user_datas) + for level, datas in new_err_warn.items(): + if datas: + err_warn[level].updates(datas) + subconfig = metaconfig + for idx, layer in enumerate(layers): + if idx: + subconfig = subconfig.config("_".join(layer)) + return metaconfig, config, err_warn -def get_output(rougailconfig, config, err_warn): +def get_output(rougailconfig, metaconfig, config, err_warn): # output if config and (not rougailconfig["cli.load_config"] or not rougailconfig["cli.read_write"]): config.property.read_only() @@ -209,6 +233,8 @@ def get_output(rougailconfig, config, err_warn): rougailconfig=rougailconfig, user_data_errors=err_warn["errors"], user_data_warnings=err_warn["warnings"], + config_owner_is_path=True, + metaconfig=metaconfig, ) return output diff --git a/src/rougail/cli/config.py b/src/rougail/cli/config.py index 77bcb50..c8729b8 100644 --- a/src/rougail/cli/config.py +++ b/src/rougail/cli/config.py @@ -41,6 +41,14 @@ cli: versions: false # {_('Displays Rougail version and all its components')} + layers: + description: {_('Open each user datas in separate layers')} + default: false + hidden: + jinja: |- + {{{{ __.step.user_data is propertyerror or __.step.user_data | length < 2 }}}} + return_type: boolean + load_config: default: true hidden: true diff --git a/tests/cli/result_user_data.txt b/tests/cli/result_user_data.txt new file mode 100644 index 0000000..98190bf --- /dev/null +++ b/tests/cli/result_user_data.txt @@ -0,0 +1,7 @@ +╭────────────── Caption ───────────────╮ +│ Variable Modified value │ +│ (⏳ Original default value) │ +╰──────────────────────────────────────╯ +Variables: +┗━━ 📓 a description: a yaml value ◀ loaded from the YAML file "yaml/file.yml" + (⏳ my_value) diff --git a/tests/cli/result_user_datas.txt b/tests/cli/result_user_datas.txt new file mode 100644 index 0000000..98190bf --- /dev/null +++ b/tests/cli/result_user_datas.txt @@ -0,0 +1,7 @@ +╭────────────── Caption ───────────────╮ +│ Variable Modified value │ +│ (⏳ Original default value) │ +╰──────────────────────────────────────╯ +Variables: +┗━━ 📓 a description: a yaml value ◀ loaded from the YAML file "yaml/file.yml" + (⏳ my_value) diff --git a/tests/cli/result_user_datas_layers.txt b/tests/cli/result_user_datas_layers.txt new file mode 100644 index 0000000..f06667e --- /dev/null +++ b/tests/cli/result_user_datas_layers.txt @@ -0,0 +1,7 @@ +╭────────────── Caption ───────────────╮ +│ Variable Modified value │ +│ (⏳ Original default value) │ +╰──────────────────────────────────────╯ +Variables: +┗━━ 📓 a description: a yaml value ◀ loaded from the YAML file "yaml/file.yml" + (⏳ my env value ◀ loaded from environment variable ⏳ my env value) diff --git a/tests/cli/yaml/file.yml b/tests/cli/yaml/file.yml new file mode 100644 index 0000000..fe56d90 --- /dev/null +++ b/tests/cli/yaml/file.yml @@ -0,0 +1,2 @@ +--- +my_variable: a yaml value diff --git a/tests/rougailcli_file/alt_console.txt b/tests/rougailcli_file/alt_console.txt index c9f2c9b..a66ad5b 100644 --- a/tests/rougailcli_file/alt_console.txt +++ b/tests/rougailcli_file/alt_console.txt @@ -1 +1 @@ -"\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503\u001b[1m \u001b[0m\u001b[1mVariable \u001b[0m\u001b[1m \u001b[0m\u2503\u001b[1m \u001b[0m\u001b[1mDescription \u001b[0m\u001b[1m \u001b[0m\u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 \u001b[1mmy_variable\u001b[0m \u2502 A description. \u2502\n\u2502 \u001b[1;7m string \u001b[0m \u001b[1;7m mandatory \u001b[0m \u2502 \u001b[1mDefault\u001b[0m: my_value \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n" \ No newline at end of file +"\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503\u001b[1m \u001b[0m\u001b[1mVariable \u001b[0m\u001b[1m \u001b[0m\u2503\u001b[1m \u001b[0m\u001b[1mDescription \u001b[0m\u001b[1m \u001b[0m\u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 \u001b[1mmy_variable\u001b[0m \u2502 A description. \u2502\n\u2502 \u001b[1;7m string \u001b[0m \u001b[1;7m mandatory \u001b[0m \u2502 \u001b[1mDefault\u001b[0m: my_value \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n" \ No newline at end of file diff --git a/tests/rougailcli_file/structures_warnings/file.yml b/tests/rougailcli_file/structures_warnings/file.yml index 453bb04..c425a8d 100644 --- a/tests/rougailcli_file/structures_warnings/file.yml +++ b/tests/rougailcli_file/structures_warnings/file.yml @@ -6,3 +6,4 @@ my_variable: validators: # validators in jinja without description makes warnings - jinja: |- {{ _.my_variable != "my_value" }} + warnings: true diff --git a/tests/test_load.py b/tests/test_load.py index 4698bd8..6dd1558 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -8,6 +8,7 @@ from rougail.cli.__main__ import main test_dir = Path(__file__).parent +os.environ['COLUMNS'] = '80' def test_cli(): @@ -19,7 +20,7 @@ def test_cli(): fh.write(ret[1]) with filename.open() as fh: data = fh.read() - assert ret == (True, data) + assert ret == (True, data), str(filename.absolute()) def test_cli_boolean(): @@ -31,7 +32,7 @@ def test_cli_boolean(): fh.write(ret[1]) with filename.open() as fh: data = fh.read() - assert ret == (True, data) + assert ret == (True, data), str(filename.absolute()) def test_cli_boolean_no(): @@ -43,7 +44,7 @@ def test_cli_boolean_no(): fh.write(ret[1]) with filename.open() as fh: data = fh.read() - assert ret == (True, data) + assert ret == (True, data), str(filename.absolute()) def test_cli_version(): @@ -56,7 +57,7 @@ def test_cli_version(): fh.write(dumps(ret)) with filename.open() as fh: data = loads(fh.read()) - assert ret == data + assert ret == data, str(filename.absolute()) def test_cli_version_user_data_disabled(): @@ -69,7 +70,7 @@ def test_cli_version_user_data_disabled(): fh.write(dumps(ret)) with filename.open() as fh: data = loads(fh.read()) - assert ret == data + assert ret == data, str(filename.absolute()) def test_cli_version_user_data_disabled_2(): @@ -81,7 +82,49 @@ def test_cli_version_user_data_disabled_2(): fh.write(dumps(ret[1])) with filename.open() as fh: data = loads(fh.read()) - assert ret == (True, data), filename + assert ret == (True, data), str(filename.absolute()) + + +def test_cli_user_data(): + with chdir(test_dir / 'cli'): + ret = main(['--main_structural_directories', 'structures', '--step.user_data', 'yaml', '--yaml.filename', 'yaml/file.yml'], do_not_print=True) + filename = Path('result_user_data.txt') + if not filename.is_file(): + with filename.open('w') as fh: + fh.write(ret[1]) + with filename.open() as fh: + data = fh.read() + assert ret == (True, data), str(filename.absolute()) + + +def test_cli_user_datas(): + save = os.environ.copy() + os.environ["MY_VARIABLE"] = "my env value" + with chdir(test_dir / 'cli'): + ret = main(['--main_structural_directories', 'structures', '--step.user_data', 'environment', 'yaml', '--yaml.filename', 'yaml/file.yml'], do_not_print=True) + filename = Path('result_user_datas.txt') + if not filename.is_file(): + with filename.open('w') as fh: + fh.write(ret[1]) + with filename.open() as fh: + data = fh.read() + assert ret == (True, data), str(filename.absolute()) + save = os.environ.copy() + + +def test_cli_user_datas_user_datas_layers(): + save = os.environ.copy() + os.environ["ROUGAIL_MY_VARIABLE"] = "my env value" + with chdir(test_dir / 'cli'): + ret = main(['--main_structural_directories', 'structures', '--cli.layers', '--step.user_data', 'environment', 'yaml', '--yaml.filename', 'yaml/file.yml'], do_not_print=True) + filename = Path('result_user_datas_layers.txt') + if not filename.is_file(): + with filename.open('w') as fh: + fh.write(ret[1]) + with filename.open() as fh: + data = fh.read() + assert ret == (True, data), str(filename.absolute()) + save = os.environ.copy() def test_cli_rougailcli(): @@ -93,10 +136,11 @@ def test_cli_rougailcli(): fh.write(dumps(ret[1])) with filename.open() as fh: data = loads(fh.read()) - assert ret == (True, data) + assert ret == (True, data), str(filename.absolute()) def test_cli_alt_rougailcli(): + save = os.environ.copy() with chdir(test_dir / 'rougailcli_file'): os.environ["ROUGAILCLI_CLI.CONFIG_FILE"] = 'alt_rougailcli.yml' ret = main([], do_not_print=True) @@ -106,10 +150,12 @@ def test_cli_alt_rougailcli(): fh.write(dumps(ret[1])) with filename.open() as fh: data = loads(fh.read()) - assert ret == (True, data) + assert ret == (True, data), str(filename.absolute()) + os.environ = save def test_cli_rougailcli_mix(): + save = os.environ.copy() with chdir(test_dir / 'rougailcli_file'): os.environ["ROUGAILCLI_CLI.CONFIG_FILE"] = 'mix_rougailcli.yml' ret = main(["-o", "doc", "--doc.output_format", "asciidoc"], do_not_print=True) @@ -119,10 +165,12 @@ def test_cli_rougailcli_mix(): fh.write(dumps(ret[1])) with filename.open() as fh: data = loads(fh.read()) - assert ret == (True, data) + assert ret == (True, data), str(filename.absolute()) + os.environ = save def test_cli_rougailcli_warning(): + save = os.environ.copy() with chdir(test_dir / 'rougailcli_file'): os.environ["ROUGAILCLI_CLI.CONFIG_FILE"] = 'warnings.yml' with warnings.catch_warnings(record=True) as rougail_wn: @@ -136,9 +184,11 @@ def test_cli_rougailcli_warning(): with filename.open() as fh: data = loads(fh.read()) assert ret == data, str(filename.absolute()) + os.environ = save def test_cli_rougailcli_not_warning(): + save = os.environ.copy() with chdir(test_dir / 'rougailcli_file'): os.environ["ROUGAILCLI_CLI.CONFIG_FILE"] = 'warnings2.yml' with warnings.catch_warnings(record=True) as rougail_wn: @@ -152,9 +202,11 @@ def test_cli_rougailcli_not_warning(): with filename.open() as fh: data = loads(fh.read()) assert ret == data, str(filename.absolute()) + os.environ = save def test_cli_rougailcli_warning2(): + save = os.environ.copy() with chdir(test_dir / 'rougailcli_file'): os.environ["ROUGAILCLI_CLI.CONFIG_FILE"] = 'warnings3.yml' with warnings.catch_warnings(record=True) as rougail_wn: @@ -164,13 +216,15 @@ def test_cli_rougailcli_warning2(): filename = Path('warnings3.txt') if not filename.is_file(): with filename.open('w') as fh: - fh.write(dumps([str(w.message) for w in rougail_wn])) + fh.write(dumps(ret)) with filename.open() as fh: data = loads(fh.read()) assert ret == data, str(filename.absolute()) + os.environ = save def test_cli_rougailcli_not_warning2(): + save = os.environ.copy() with chdir(test_dir / 'rougailcli_file'): os.environ["ROUGAILCLI_CLI.CONFIG_FILE"] = 'warnings4.yml' with warnings.catch_warnings(record=True) as rougail_wn: @@ -184,3 +238,4 @@ def test_cli_rougailcli_not_warning2(): with filename.open() as fh: data = loads(fh.read()) assert ret == data, str(filename.absolute()) + os.environ = save