Compare commits

..

1 commit

Author SHA1 Message Date
ba9efe9dcb feat: add frozen variable support 2025-11-26 20:44:59 +01:00
6 changed files with 113 additions and 177 deletions

View file

@ -1,14 +1,2 @@
# Define user datas interactivly # rougail-user-data-questionary
> **🛈 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-27 21:49+0100\n" "POT-Creation-Date: 2025-11-06 06:11+0100\n"
"PO-Revision-Date: 2025-11-27 21:49+0100\n" "PO-Revision-Date: 2025-11-06 06:11+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: fr\n" "Language: fr\n"
@ -14,32 +14,16 @@ 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.8\n" "X-Generator: Poedit 3.7\n"
#: src/rougail/user_data_questionary/config.py:32 #: src/rougail/user_data_questionary/config.py:31
msgid "Define user datas interactivly" msgid "Define values interactivly"
msgstr "Définir des données utilisateur interactivement" msgstr "Définir des valeurs interactivement"
#: src/rougail/user_data_questionary/config.py:38 #: src/rougail/user_data_questionary/config.py:39
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:46 #: src/rougail/user_data_questionary/config.py:44
msgid "Show secrets instead of obscuring them" msgid "Show secrets instead of obscuring them"
msgstr "Afficher les secrets plutôt que de les obscurcir" msgstr "Afficher les secrets plutôt que des 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-27 21:50+0100\n" "POT-Creation-Date: 2025-11-06 06:11+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,31 +15,15 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
#: src/rougail/user_data_questionary/config.py:32 #: src/rougail/user_data_questionary/config.py:31
msgid "Define user datas interactivly" msgid "Define values interactivly"
msgstr "" msgstr ""
#: src/rougail/user_data_questionary/config.py:38 #: src/rougail/user_data_questionary/config.py:39
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:46 #: src/rougail/user_data_questionary/config.py:44
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

@ -29,13 +29,12 @@ def get_rougail_config(
"""generate rougail config""" """generate rougail config"""
options = f""" options = f"""
questionary: questionary:
description: {_("Define user datas interactivly")} description: {_("Define values 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")}

View file

@ -38,8 +38,6 @@ from .i18n import _
class RougailUserDataQuestionary: class RougailUserDataQuestionary:
"""Rougail userdata for Questionary"""
interactive_user_datas = True interactive_user_datas = True
def __init__( def __init__(
@ -58,24 +56,41 @@ class RougailUserDataQuestionary:
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.errors = [] # self.config.property.read_write()
self.warnings = []
self.questionary_show_secrets = self.rougailconfig["questionary.show_secrets"]
if "demoting_error_warning" not in self.config.property.get(): 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"]:
self.parse_mandatories() 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)
else: else:
old_path_in_description = self.config.information.get( old_path_in_description = self.config.information.get(
"path_in_description", True "path_in_description", True
@ -94,26 +109,6 @@ class RougailUserDataQuestionary:
} }
] ]
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 display_title = True
for option in config: for option in config:
@ -131,21 +126,40 @@ class RougailUserDataQuestionary:
def display_questionary(self, option, title_level): 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 = option_type RougailValidator.option_type = {
"type": option_type,
"func": type_obj,
}
RougailValidator.ismulti = ismulti RougailValidator.ismulti = ismulti
ori_default = default
args = [" " * title_level + "📓 " + option.description()] args = [" " * title_level + "📓 " + option.description()]
if "frozen" in option.property.get(): if "frozen" in option.property.get():
self._display_questionary_frozen(args[0], default) args[0] += " " + str(default)
question_funtion = qprint
qprint(" " + args[0])
return return
question_funtion = self._dispatcher_questionary( if option_type == "choice":
option_type, RougailValidator, kwargs, option, default question_funtion = select
) RougailValidator.default = default
ori_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
if ismulti: if ismulti:
kwargs["multiline"] = True kwargs["multiline"] = True
if default: if default:
@ -157,102 +171,69 @@ class RougailUserDataQuestionary:
value = RougailValidator().convert_value( value = RougailValidator().convert_value(
question_funtion(*args, **kwargs).ask(), False question_funtion(*args, **kwargs).ask(), False
) )
if option.owner.isdefault() and value == ori_default: if 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( def convert_value(
self, self,
document, document,
set_value=True, validate=True,
) -> any: ):
"""Valid value""" if not self.ismulti:
self.set_value = set_value value = self._convert_a_value(document, document, validate)
self.document = document else:
self.func = CONVERT_OPTION.get(self.option_type, {}).get("func") value = []
convert_func = ( if document is not None:
self._convert_multi_value if self.ismulti else self._convert_a_value for val in document.strip().split("\n"):
) val = self._convert_a_value(val, document, validate)
value = convert_func(document) if val is not None:
if self.set_value: value.append(val)
self._set_value(value) if validate:
try:
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),
) from err
return value return value
def _convert_multi_value(self, value: any) -> list[any]: def _convert_a_value(self, value, document, validate):
if self.document is None: if value is None:
return [] if self.option_type["type"] == "choice":
value = [] return self.default
for val in self.document.strip().split("\n"): return None
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 in [None, ""]: if value == "":
return self._convert_empty_value() if validate and "mandatory" in self.option.property.get():
if not self.func: raise ValidationError(
return value message=_("Value must not be empty"),
try: cursor_position=len(document),
return self.func(value) )
except: return None
msg = _("Not a valid {0}").format(self.option_type) if self.option_type["func"]:
self._raise_validation_error(msg) try:
return self.option_type["func"](value)
def _convert_empty_value(self) -> any: except Exception as err:
if self.option_type == "choice": raise ValidationError(
return self.default message=_("Not a valid {0}").format(self.option_type["type"]),
self._validate_empty_value() cursor_position=len(document),
return None ) from err
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),
)