Compare commits

...

4 commits

10 changed files with 279 additions and 139 deletions

View file

@ -1,2 +1,14 @@
# rougail-user-data-questionary # Define user datas interactivly
> **🛈 Informations**
>
> **<a id="questionary" name="questionary">questionary</a>**\
> `standard` *`disabled`*\
> **Disabled**: when questionary is not set in "[Select for user_data](#step.user_data)"
| Variable&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Description&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **<a id="questionary.mandatory" name="questionary.mandatory">questionary.mandatory</a>**<br/>[`boolean`](https://rougail.readthedocs.io/en/latest/variable.html#variables-types) `standard` `mandatory`<br/>**Command line**: <br/><a id="questionary.mandatory" name="questionary.mandatory">--questionary.mandatory</a><br/><a id="questionary.mandatory" name="questionary.mandatory">--questionary.no-mandatory</a><br/>• -qm<br/>• -nqm<br/>**Environment variable**: <a id="questionary.mandatory" name="questionary.mandatory">ROUGAILCLI_QUESTIONARY.MANDATORY</a> | Ask values only for mandatories variables without any value.<br/>**Default**: false |
| **<a id="questionary.show_secrets" name="questionary.show_secrets">questionary.show_secrets</a>**<br/>[`boolean`](https://rougail.readthedocs.io/en/latest/variable.html#variables-types) `standard` `mandatory`<br/>**Command line**: <br/><a id="questionary.show_secrets" name="questionary.show_secrets">--questionary.show_secrets</a><br/><a id="questionary.show_secrets" name="questionary.show_secrets">--questionary.no-show_secrets</a><br/>• -qs<br/>• -nqs<br/>**Environment variable**: <a id="questionary.show_secrets" name="questionary.show_secrets">ROUGAILCLI_QUESTIONARY.SHOW_SECRETS</a> | Show secrets instead of obscuring them.<br/>**Default**: false |

View file

@ -5,8 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2025-11-06 06:11+0100\n" "POT-Creation-Date: 2025-11-27 21:49+0100\n"
"PO-Revision-Date: 2025-11-06 06:11+0100\n" "PO-Revision-Date: 2025-11-27 21:49+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: fr\n" "Language: fr\n"
@ -14,16 +14,32 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.7\n" "X-Generator: Poedit 3.8\n"
#: src/rougail/user_data_questionary/config.py:31 #: src/rougail/user_data_questionary/config.py:32
msgid "Define values interactivly" msgid "Define user datas interactivly"
msgstr "Définir des valeurs interactivement" msgstr "Définir des données utilisateur interactivement"
#: src/rougail/user_data_questionary/config.py:39 #: src/rougail/user_data_questionary/config.py:38
msgid "when questionary is not set in \"step.user_data\""
msgstr "lorsque questionary n'est pas mis dans step.user_data"
#: src/rougail/user_data_questionary/config.py:41
msgid "Ask values only for mandatories variables without any value" msgid "Ask values only for mandatories variables without any value"
msgstr "Demander des valeurs uniquement pour des variables obligatoires sans valeur" msgstr "Demander des valeurs uniquement pour des variables obligatoires sans valeur"
#: src/rougail/user_data_questionary/config.py:44 #: src/rougail/user_data_questionary/config.py:46
msgid "Show secrets instead of obscuring them" msgid "Show secrets instead of obscuring them"
msgstr "Afficher les secrets plutôt que des obscurcir." msgstr "Afficher les secrets plutôt que de les obscurcir"
#: src/rougail/user_data_questionary/data.py:61
msgid "questionary is not set in step.user_data"
msgstr "questionary n'est pas mis dans step.user_data"
#: src/rougail/user_data_questionary/data.py:229
msgid "Not a valid {0}"
msgstr "N'est pas un valide {0}"
#: src/rougail/user_data_questionary/data.py:250
msgid "Value must not be empty"
msgstr "La valeur ne doit pas être vide"

View file

@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2025-11-06 06:11+0100\n" "POT-Creation-Date: 2025-11-27 21:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -15,15 +15,31 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
#: src/rougail/user_data_questionary/config.py:31 #: src/rougail/user_data_questionary/config.py:32
msgid "Define values interactivly" msgid "Define user datas interactivly"
msgstr "" msgstr ""
#: src/rougail/user_data_questionary/config.py:39 #: src/rougail/user_data_questionary/config.py:38
msgid "when questionary is not set in \"step.user_data\""
msgstr ""
#: src/rougail/user_data_questionary/config.py:41
msgid "Ask values only for mandatories variables without any value" msgid "Ask values only for mandatories variables without any value"
msgstr "" msgstr ""
#: src/rougail/user_data_questionary/config.py:44 #: src/rougail/user_data_questionary/config.py:46
msgid "Show secrets instead of obscuring them" msgid "Show secrets instead of obscuring them"
msgstr "" msgstr ""
#: src/rougail/user_data_questionary/data.py:61
msgid "questionary is not set in step.user_data"
msgstr ""
#: src/rougail/user_data_questionary/data.py:229
msgid "Not a valid {0}"
msgstr ""
#: src/rougail/user_data_questionary/data.py:250
msgid "Value must not be empty"
msgstr ""

View file

@ -1,5 +1,26 @@
"""
Silique (https://www.silique.fr)
Copyright (C) 2024-2025
distribued with GPL-2 or later license
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
from .data import RougailUserDataQuestionary from .data import RougailUserDataQuestionary
from .__version__ import __version__ from .__version__ import __version__
RougailUserData = RougailUserDataQuestionary RougailUserData = RougailUserDataQuestionary
__all__ = ('RougailUserDataQuestionary',) __all__ = ("RougailUserDataQuestionary",)

View file

@ -1 +1,22 @@
"""
Silique (https://www.silique.fr)
Copyright (C) 2024-2025
distribued with GPL-2 or later license
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
__version__ = "0.1.0a1" __version__ = "0.1.0a1"

View file

@ -26,14 +26,16 @@ def get_rougail_config(
*, *,
backward_compatibility=True, backward_compatibility=True,
) -> dict: ) -> dict:
"""generate rougail config"""
options = f""" options = f"""
questionary: questionary:
description: {_("Define values interactivly")} description: {_("Define user datas interactivly")}
disabled: disabled:
jinja: | jinja: |
{{% if step.user_data is not propertyerror and 'questionary' not in step.user_data %}} {{% if step.user_data is not propertyerror and 'questionary' not in step.user_data %}}
disabled disabled
{{% endif %}} {{% endif %}}
description: {_('when questionary is not set in "step.user_data"')}
mandatory: mandatory:
description: {_("Ask values only for mandatories variables without any value")} description: {_("Ask values only for mandatories variables without any value")}
@ -46,11 +48,12 @@ questionary:
type: boolean type: boolean
default: false default: false
""" """
return {'name': 'questionary', return {
'process': 'user data', "name": "questionary",
'options': options, "process": "user data",
'level': 60, "options": options,
} "level": 60,
}
__all__ = ('get_rougail_config',) __all__ = ("get_rougail_config",)

View file

@ -18,16 +18,28 @@ You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
""" """
import warnings
from questionary import text, select, confirm, password, Validator, ValidationError, print as qprint
import warnings
from questionary import (
text,
select,
confirm,
password,
Validator,
ValidationError,
print as qprint,
)
from tiramisu.error import ValueOptionError, ValueErrorWarning, ValueWarning
from rougail.tiramisu import CONVERT_OPTION from rougail.tiramisu import CONVERT_OPTION
from rougail.config import RougailConfig from rougail.config import RougailConfig
from rougail.error import ExtensionError from rougail.error import ExtensionError
from tiramisu.error import ValueOptionError, ValueErrorWarning, ValueWarning
from .i18n import _
class RougailUserDataQuestionary: class RougailUserDataQuestionary:
"""Rougail userdata for Questionary"""
interactive_user_datas = True interactive_user_datas = True
def __init__( def __init__(
@ -40,170 +52,207 @@ class RougailUserDataQuestionary:
self.config = config self.config = config
if rougailconfig is None: if rougailconfig is None:
rougailconfig = RougailConfig rougailconfig = RougailConfig
user_data = rougailconfig['step.user_data'] user_data = rougailconfig["step.user_data"]
if 'questionary' not in user_data: if "questionary" not in user_data:
user_data.append('questionary') user_data.append("questionary")
rougailconfig['step.user_data'] = user_data rougailconfig["step.user_data"] = user_data
user_data = rougailconfig['step.user_data'] user_data = rougailconfig["step.user_data"]
if 'questionary' not in user_data: if "questionary" not in user_data:
raise ExtensionError('questionary is not set in step.user_data') raise ExtensionError(_("questionary is not set in step.user_data"))
self.rougailconfig = rougailconfig self.rougailconfig = rougailconfig
self.errors = []
self.warnings = []
warnings.simplefilter("always", ValueErrorWarning) warnings.simplefilter("always", ValueErrorWarning)
warnings.simplefilter("always", ValueWarning) warnings.simplefilter("always", ValueWarning)
def run( def run(
self, self,
) -> None: ) -> None:
# self.config.property.read_write() self.errors = []
if 'demoting_error_warning' not in self.config.property.get(): self.warnings = []
self.questionary_show_secrets = self.rougailconfig["questionary.show_secrets"]
if "demoting_error_warning" not in self.config.property.get():
add_demoting = True add_demoting = True
self.config.property.add('demoting_error_warning') self.config.property.add("demoting_error_warning")
else: else:
add_demoting = False add_demoting = False
if self.rougailconfig['questionary.mandatory']: if self.rougailconfig["questionary.mandatory"]:
current_titles = [] self.parse_mandatories()
while True:
mandatories = self.config.value.mandatory()
if not mandatories:
break
mandatory = mandatories[0]
path = mandatory.path()
if '.' in path:
current_config = self.config
for idx, p in enumerate(path.split('.')[0:-1]):
current_config = current_config.option(p)
if idx < len(current_titles):
if current_titles[idx] == p:
continue
current_titles = current_titles[0:idx]
current_titles.append(p)
self.print(current_config.description(), idx)
self.display_questionary(mandatory)
else: else:
old_path_in_description = self.config.information.get(
"path_in_description", True
)
self.config.information.set("path_in_description", False)
self.parse(self.config) self.parse(self.config)
self.config.information.set("path_in_description", old_path_in_description)
if add_demoting: if add_demoting:
self.config.property.remove('demoting_error_warning') self.config.property.remove("demoting_error_warning")
return [ return [
{ {
"source": 'Questionary', "source": "Questionary",
"errors": self.errors, "errors": self.errors,
"warnings": self.warnings, "warnings": self.warnings,
"values": [], "values": [],
} }
] ]
def parse_mandatories(self):
current_titles = []
while True:
mandatories = self.config.value.mandatory()
if not mandatories:
break
mandatory = mandatories[0]
path = mandatory.path()
if "." in path:
current_config = self.config
for idx, p in enumerate(path.split(".")[0:-1]):
current_config = current_config.option(p)
if idx < len(current_titles):
if current_titles[idx] == p:
continue
current_titles = current_titles[0:idx]
current_titles.append(p)
self.print(current_config.description(), idx)
self.display_questionary(mandatory, title_level=0)
def parse(self, config, title_level=0): def parse(self, config, title_level=0):
display_title = True
for option in config: for option in config:
if title_level and display_title:
self.print(config.description(), title_level)
display_title = False
if option.isoptiondescription(): if option.isoptiondescription():
self.print(option.description(), title_level)
self.parse(option, title_level + 1) self.parse(option, title_level + 1)
else: else:
self.display_questionary(option) self.display_questionary(option, title_level)
def print(self, title, title_level): def print(self, title, title_level):
qprint(' ' * title_level + '📂 ' + title, 'bold') qprint(" " * title_level + "📂 " + title, "bold")
def display_questionary(self, option): def display_questionary(self, option, title_level):
kwargs = {} kwargs = {}
option_type = option.information.get('type') option_type = option.information.get("type")
isdefault = option.owner.isdefault()
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("ignore") warnings.simplefilter("ignore")
default = option.value.get() default = option.value.get()
ismulti = option.ismulti() ismulti = option.ismulti()
type_obj = None
type_obj = CONVERT_OPTION.get(option_type, {}).get("func")
RougailValidator.option = option RougailValidator.option = option
RougailValidator.option_type = {'type': option_type, RougailValidator.option_type = option_type
'func': type_obj,
}
RougailValidator.ismulti = ismulti RougailValidator.ismulti = ismulti
args = [" " * title_level + "📓 " + option.description()]
if "frozen" in option.property.get():
self._display_questionary_frozen(args[0], default)
return
question_funtion = self._dispatcher_questionary(
option_type, RougailValidator, kwargs, option, default
)
ori_default = default ori_default = default
if option_type == 'choice':
question_funtion = select
RougailValidator.default = default
kwargs['choices'] = option.value.list()
elif option_type == 'boolean':
question_funtion = confirm
elif option_type == 'secret' and not self.rougailconfig['questionary.show_secrets']:
question_funtion = password
else:
question_funtion = text
kwargs['validate'] = RougailValidator
args = ['📓 ' + option.description()]
if ismulti: if ismulti:
kwargs['multiline'] = True kwargs["multiline"] = True
if default: if default:
kwargs['default'] = "\n".join([str(d) for d in default]) kwargs["default"] = "\n".join([str(d) for d in default])
elif default is not None: elif default is not None:
if isinstance(default, (int, float)): if isinstance(default, (int, float)):
default = str(default) default = str(default)
kwargs['default'] = default kwargs["default"] = default
value = RougailValidator().convert_value(question_funtion(*args, **kwargs).ask(), False) value = RougailValidator().convert_value(
if isdefault and value == ori_default: question_funtion(*args, **kwargs).ask(), False
)
if option.owner.isdefault() and value == ori_default:
option.value.reset() option.value.reset()
else: else:
option.value.set(value) option.value.set(value)
def _dispatcher_questionary(
self, option_type: str, RougailValidator, kwargs: dict, option, default: any
) -> callable:
if option_type == "choice":
RougailValidator.default = default
kwargs["choices"] = option.value.list()
return select
if option_type == "boolean":
return confirm
if option_type == "secret" and not self.questionary_show_secrets:
return password
kwargs["validate"] = RougailValidator
return text
def _display_questionary_frozen(self, args: str, default: any) -> None:
qprint(" " + args + " " + str(default))
class RougailValidator(Validator): class RougailValidator(Validator):
"""Extend Questionary Validator to Rougail needs
Be careful it's a singleton
"""
def validate(self, document): def validate(self, document):
"""patch Questionary validate"""
return self.convert_value(document.text) return self.convert_value(document.text)
def convert_value(self, def convert_value(
document, self,
validate=True, document,
): set_value=True,
if not self.ismulti: ) -> any:
value = self._convert_a_value(document, document, validate) """Valid value"""
else: self.set_value = set_value
value = [] self.document = document
if document is not None: self.func = CONVERT_OPTION.get(self.option_type, {}).get("func")
for val in document.strip().split('\n'): convert_func = (
val = self._convert_a_value(val, document, validate) self._convert_multi_value if self.ismulti else self._convert_a_value
if val is not None: )
value.append(val) value = convert_func(document)
if validate: if self.set_value:
try: self._set_value(value)
with warnings.catch_warnings(record=True) as warns:
self.option.value.set(value)
for warn in warns:
if isinstance(warn.message, ValueErrorWarning):
warn.message.prefix = ''
raise ValidationError(
message=str(warn.message),
cursor_position=len(document),
)
except ValueOptionError as err:
err.prefix = ''
raise ValidationError(
message=str(err),
cursor_position=len(document),
)
return value return value
def _convert_a_value(self, value, document, validate): def _convert_multi_value(self, value: any) -> list[any]:
if value is None: if self.document is None:
if self.option_type['type'] == 'choice': return []
return self.default value = []
return for val in self.document.strip().split("\n"):
val = self._convert_a_value(val)
if val is not None:
value.append(val)
return value
def _convert_a_value(self, value: any) -> any:
if isinstance(value, str): if isinstance(value, str):
value = value.strip() value = value.strip()
if value == '': if value in [None, ""]:
if validate and "mandatory" in self.option.property.get(): return self._convert_empty_value()
raise ValidationError( if not self.func:
message=f"Value must not be empty", return value
cursor_position=len(document), try:
) return self.func(value)
return except:
if self.option_type['func']: msg = _("Not a valid {0}").format(self.option_type)
try: self._raise_validation_error(msg)
return self.option_type['func'](value)
except: def _convert_empty_value(self) -> any:
raise ValidationError( if self.option_type == "choice":
message=f"Not a valid {self.option_type['type']}", return self.default
cursor_position=len(document), self._validate_empty_value()
) return None
return value
def _set_value(self, value: any) -> None:
try:
with warnings.catch_warnings(record=True) as warns:
self.option.value.set(value)
except ValueOptionError as err:
self._raise_validation_error(err)
for warn in warns:
if isinstance(warn.message, ValueErrorWarning):
self._raise_validation_error(warn.message)
def _validate_empty_value(self) -> None:
if self.set_value and "mandatory" in self.option.property.get():
self._raise_validation_error(_("Value must not be empty"))
def _raise_validation_error(self, err) -> None:
if not isinstance(err, str):
err.prefix = ""
raise ValidationError(
message=str(err),
cursor_position=len(self.document),
)

View file

@ -20,7 +20,9 @@ from gettext import translation
from pathlib import Path from pathlib import Path
t = translation( t = translation(
"rougail_user_data_questionary", str(Path(__file__).parent / "locale"), fallback=True "rougail_user_data_questionary",
str(Path(__file__).parent / "locale"),
fallback=True,
) )
_ = t.gettext _ = t.gettext