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,43 +56,23 @@ 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()
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.config.information.set("path_in_description", old_path_in_description)
if add_demoting:
self.config.property.remove("demoting_error_warning")
return [
{
"source": "Questionary",
"errors": self.errors,
"warnings": self.warnings,
"values": [],
}
]
def parse_mandatories(self):
current_titles = [] current_titles = []
while True: while True:
mandatories = self.config.value.mandatory() mandatories = self.config.value.mandatory()
@ -113,6 +91,23 @@ class RougailUserDataQuestionary:
current_titles.append(p) current_titles.append(p)
self.print(current_config.description(), idx) self.print(current_config.description(), idx)
self.display_questionary(mandatory, title_level=0) self.display_questionary(mandatory, title_level=0)
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.config.information.set("path_in_description", old_path_in_description)
if add_demoting:
self.config.property.remove("demoting_error_warning")
return [
{
"source": "Questionary",
"errors": self.errors,
"warnings": self.warnings,
"values": [],
}
]
def parse(self, config, title_level=0): def parse(self, config, title_level=0):
display_title = True display_title = True
@ -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")
convert_func = (
self._convert_multi_value if self.ismulti else self._convert_a_value
)
value = convert_func(document)
if self.set_value:
self._set_value(value)
return value
def _convert_multi_value(self, value: any) -> list[any]:
if self.document is None:
return []
value = [] value = []
for val in self.document.strip().split("\n"): if document is not None:
val = self._convert_a_value(val) for val in document.strip().split("\n"):
val = self._convert_a_value(val, document, validate)
if val is not None: if val is not None:
value.append(val) value.append(val)
return value if validate:
def _convert_a_value(self, value: any) -> any:
if isinstance(value, str):
value = value.strip()
if value in [None, ""]:
return self._convert_empty_value()
if not self.func:
return value
try:
return self.func(value)
except:
msg = _("Not a valid {0}").format(self.option_type)
self._raise_validation_error(msg)
def _convert_empty_value(self) -> any:
if self.option_type == "choice":
return self.default
self._validate_empty_value()
return None
def _set_value(self, value: any) -> None:
try: try:
with warnings.catch_warnings(record=True) as warns: with warnings.catch_warnings(record=True) as warns:
self.option.value.set(value) self.option.value.set(value)
except ValueOptionError as err:
self._raise_validation_error(err)
for warn in warns: for warn in warns:
if isinstance(warn.message, ValueErrorWarning): if isinstance(warn.message, ValueErrorWarning):
self._raise_validation_error(warn.message) warn.message.prefix = ""
raise ValidationError(
def _validate_empty_value(self) -> None: message=str(warn.message),
if self.set_value and "mandatory" in self.option.property.get(): cursor_position=len(document),
self._raise_validation_error(_("Value must not be empty")) )
except ValueOptionError as err:
def _raise_validation_error(self, err) -> None:
if not isinstance(err, str):
err.prefix = "" err.prefix = ""
raise ValidationError( raise ValidationError(
message=str(err), message=str(err),
cursor_position=len(self.document), cursor_position=len(document),
) from err
return value
def _convert_a_value(self, value, document, validate):
if value is None:
if self.option_type["type"] == "choice":
return self.default
return None
if isinstance(value, str):
value = value.strip()
if value == "":
if validate and "mandatory" in self.option.property.get():
raise ValidationError(
message=_("Value must not be empty"),
cursor_position=len(document),
) )
return None
if self.option_type["func"]:
try:
return self.option_type["func"](value)
except Exception as err:
raise ValidationError(
message=_("Not a valid {0}").format(self.option_type["type"]),
cursor_position=len(document),
) from err
return value