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
value = [] self.func = CONVERT_OPTION.get(self.option_type, {}).get("func")
if document is not None: convert_func = (
for val in document.strip().split("\n"): self._convert_multi_value if self.ismulti else self._convert_a_value
val = self._convert_a_value(val, document, validate) )
if val is not None: value = convert_func(document)
value.append(val) if self.set_value:
if validate: self._set_value(value)
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_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 None 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=_("Value must not be empty"), return value
cursor_position=len(document), try:
) return self.func(value)
return None 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 Exception as err: def _convert_empty_value(self) -> any:
raise ValidationError( if self.option_type == "choice":
message=_("Not a valid {0}").format(self.option_type["type"]), return self.default
cursor_position=len(document), self._validate_empty_value()
) from err 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),
)