Compare commits

..

4 commits

6 changed files with 177 additions and 113 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

@ -29,12 +29,13 @@ def get_rougail_config(
"""generate rougail config""" """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")}

View file

@ -38,6 +38,8 @@ from .i18n import _
class RougailUserDataQuestionary: class RougailUserDataQuestionary:
"""Rougail userdata for Questionary"""
interactive_user_datas = True interactive_user_datas = True
def __init__( def __init__(
@ -56,41 +58,24 @@ 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.config.property.read_write() self.errors = []
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"]:
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, 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
@ -109,6 +94,26 @@ 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:
@ -126,40 +131,21 @@ 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 = { RougailValidator.option_type = 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():
args[0] += " " + str(default) self._display_questionary_frozen(args[0], default)
question_funtion = qprint
qprint(" " + args[0])
return return
if option_type == "choice": question_funtion = self._dispatcher_questionary(
question_funtion = select option_type, RougailValidator, kwargs, option, default
RougailValidator.default = default )
kwargs["choices"] = option.value.list() ori_default = default
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:
@ -171,69 +157,102 @@ class RougailUserDataQuestionary:
value = RougailValidator().convert_value( value = RougailValidator().convert_value(
question_funtion(*args, **kwargs).ask(), False question_funtion(*args, **kwargs).ask(), False
) )
if isdefault and value == ori_default: 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( def convert_value(
self, self,
document, document,
validate=True, set_value=True,
): ) -> any:
if not self.ismulti: """Valid value"""
value = self._convert_a_value(document, document, validate) self.set_value = set_value
else: self.document = document
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 = []
if document is not None: for val in self.document.strip().split("\n"):
for val in document.strip().split("\n"): val = self._convert_a_value(val)
val = self._convert_a_value(val, document, validate)
if val is not None: if val is not None:
value.append(val) value.append(val)
if validate: return value
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):
warn.message.prefix = "" self._raise_validation_error(warn.message)
raise ValidationError(
message=str(warn.message), def _validate_empty_value(self) -> None:
cursor_position=len(document), if self.set_value and "mandatory" in self.option.property.get():
) 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(document), cursor_position=len(self.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