mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[mod] refactor the whole argument parsing flow into a component oriented way
This commit is contained in:
parent
0aebd575f7
commit
05aa54ac0f
2 changed files with 186 additions and 114 deletions
|
@ -2391,6 +2391,184 @@ def _parse_args_for_action(action, args={}):
|
|||
return _parse_args_in_yunohost_format(args, action_args)
|
||||
|
||||
|
||||
class Question:
|
||||
"empty class to store questions information"
|
||||
|
||||
|
||||
class YunoHostArgumentFormatParser(object):
|
||||
hide_user_input_in_prompt = False
|
||||
|
||||
def parse_question(self, question, user_answers):
|
||||
parsed_question = Question()
|
||||
|
||||
parsed_question.name = question['name']
|
||||
parsed_question.default = question.get('default', None)
|
||||
parsed_question.choices = question.get('choices', [])
|
||||
parsed_question.optional = question.get('optional', False)
|
||||
parsed_question.ask = question.get('ask')
|
||||
parsed_question.value = user_answers.get(parsed_question.name)
|
||||
|
||||
if parsed_question.ask is None:
|
||||
parsed_question.ask = "Enter value for '%s':" % parsed_question.name
|
||||
|
||||
return parsed_question
|
||||
|
||||
def parse(self, question, user_answers):
|
||||
question = self.parse_question(question, user_answers)
|
||||
|
||||
if question.value is None:
|
||||
text_for_user_input_in_cli = self._format_text_for_user_input_in_cli(question)
|
||||
|
||||
try:
|
||||
question.value = msignals.prompt(text_for_user_input_in_cli, self.hide_user_input_in_prompt)
|
||||
except NotImplementedError:
|
||||
question.value = None
|
||||
|
||||
# we don't have an answer, check optional and default_value
|
||||
if question.value is None:
|
||||
if not question.optional and question.default is None:
|
||||
raise YunohostError('app_argument_required', name=question.name)
|
||||
else:
|
||||
question.value = self.default_value if question.default is None else question.default
|
||||
|
||||
# we have an answer, do some post checks
|
||||
if question.value is not None:
|
||||
if question.choices and question.value not in question.choices:
|
||||
raise YunohostError('app_argument_choice_invalid', name=question.name,
|
||||
choices=', '.join(question.choices))
|
||||
|
||||
# this is done to enforce a certain formating like for boolean
|
||||
# by default it doesn't do anything
|
||||
question.value = self._post_parse_value(question)
|
||||
|
||||
return (question.value, self.argument_type)
|
||||
|
||||
def _format_text_for_user_input_in_cli(self, question):
|
||||
text_for_user_input_in_cli = _value_for_locale(question.ask)
|
||||
|
||||
if question.default is not None:
|
||||
text_for_user_input_in_cli += ' (default: {0})'.format(question.default)
|
||||
|
||||
if question.choices:
|
||||
text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question.choices))
|
||||
|
||||
return text_for_user_input_in_cli
|
||||
|
||||
def _post_parse_value(self, question):
|
||||
return question.value
|
||||
|
||||
|
||||
class StringArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "string"
|
||||
default_value = ""
|
||||
|
||||
|
||||
class PasswordArgumentParser(YunoHostArgumentFormatParser):
|
||||
hide_user_input_in_prompt = True
|
||||
argument_type = "password"
|
||||
default_value = ""
|
||||
|
||||
|
||||
class PathArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "path"
|
||||
default_value = ""
|
||||
|
||||
|
||||
class BooleanArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "boolean"
|
||||
default_value = False
|
||||
|
||||
def parse_question(self, question, user_answers):
|
||||
question = super(BooleanArgumentParser, self).parse_question(question, user_answers)
|
||||
|
||||
if question.default is None:
|
||||
question.default = False
|
||||
|
||||
return question
|
||||
|
||||
def _format_text_for_user_input_in_cli(self, question):
|
||||
text_for_user_input_in_cli = _value_for_locale(question.ask)
|
||||
|
||||
text_for_user_input_in_cli += " [yes | no]"
|
||||
|
||||
if question.default is not None:
|
||||
formatted_default = "yes" if question.default else "no"
|
||||
text_for_user_input_in_cli += ' (default: {0})'.format(formatted_default)
|
||||
|
||||
return text_for_user_input_in_cli
|
||||
|
||||
def _post_parse_value(self, question):
|
||||
if isinstance(question.value, bool):
|
||||
return 1 if question.value else 0
|
||||
|
||||
if str(question.value).lower() in ["1", "yes", "y"]:
|
||||
return 1
|
||||
|
||||
if str(question.value).lower() in ["0", "no", "n"]:
|
||||
return 0
|
||||
|
||||
raise YunohostError('app_argument_choice_invalid', name=question.name,
|
||||
choices='yes, no, y, n, 1, 0')
|
||||
|
||||
|
||||
class DomainArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "domain"
|
||||
|
||||
def parse_question(self, question, user_answers):
|
||||
from yunohost.domain import domain_list, _get_maindomain
|
||||
|
||||
question = super(DomainArgumentParser, self).parse_question(question, user_answers)
|
||||
|
||||
if question.default is None:
|
||||
question.default = _get_maindomain()
|
||||
|
||||
question.choices = domain_list()["domains"]
|
||||
|
||||
return question
|
||||
|
||||
|
||||
class UserArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "user"
|
||||
|
||||
def parse_question(self, question, user_answers):
|
||||
from yunohost.user import user_list
|
||||
|
||||
question = super(UserArgumentParser, self).parse_question(question, user_answers)
|
||||
question.choices = user_list()["users"]
|
||||
|
||||
return question
|
||||
|
||||
|
||||
class AppArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "app"
|
||||
|
||||
def parse_question(self, question, user_answers):
|
||||
from yunohost.app import app_list
|
||||
|
||||
question = super(AppArgumentParser, self).parse_question(question, user_answers)
|
||||
question.choices = [x["id"] for x in app_list()["apps"]]
|
||||
|
||||
return question
|
||||
|
||||
|
||||
class DisplayTextArgumentParser(YunoHostArgumentFormatParser):
|
||||
|
||||
def parse(self, question, user_answers):
|
||||
print(question["ask"])
|
||||
|
||||
|
||||
ARGUMENTS_TYPE_PARSERS = {
|
||||
"string": StringArgumentParser,
|
||||
"password": PasswordArgumentParser,
|
||||
"path": PathArgumentParser,
|
||||
"boolean": BooleanArgumentParser,
|
||||
"domain": DomainArgumentParser,
|
||||
"user": UserArgumentParser,
|
||||
"app": AppArgumentParser,
|
||||
"display_text": DisplayTextArgumentParser,
|
||||
}
|
||||
|
||||
|
||||
def _parse_args_in_yunohost_format(user_answers, argument_questions):
|
||||
"""Parse arguments store in either manifest.json or actions.json or from a
|
||||
config panel against the user answers when they are present.
|
||||
|
@ -2402,121 +2580,14 @@ def _parse_args_in_yunohost_format(user_answers, argument_questions):
|
|||
format from actions.json/toml, manifest.json/toml
|
||||
or config_panel.json/toml
|
||||
"""
|
||||
from yunohost.domain import domain_list, _get_maindomain
|
||||
from yunohost.user import user_list
|
||||
|
||||
parsed_answers_dict = OrderedDict()
|
||||
|
||||
for question in argument_questions:
|
||||
question_name = question['name']
|
||||
question_type = question.get('type', 'string')
|
||||
question_default = question.get('default', None)
|
||||
question_choices = question.get('choices', [])
|
||||
question_value = None
|
||||
parser = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")]()
|
||||
|
||||
# Transpose default value for boolean type and set it to
|
||||
# false if not defined.
|
||||
if question_type == 'boolean':
|
||||
question_default = 1 if question_default else 0
|
||||
|
||||
# do not print for webadmin
|
||||
if question_type == 'display_text' and msettings.get('interface') != 'api':
|
||||
print(_value_for_locale(question['ask']))
|
||||
continue
|
||||
|
||||
# Attempt to retrieve argument value
|
||||
if question_name in user_answers:
|
||||
question_value = user_answers[question_name]
|
||||
else:
|
||||
if 'ask' in question:
|
||||
# Retrieve proper ask string
|
||||
text_for_user_input_in_cli = _value_for_locale(question['ask'])
|
||||
|
||||
# Append extra strings
|
||||
if question_type == 'boolean':
|
||||
text_for_user_input_in_cli += ' [yes | no]'
|
||||
elif question_choices:
|
||||
text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question_choices))
|
||||
|
||||
if question_default is not None:
|
||||
if question_type == 'boolean':
|
||||
text_for_user_input_in_cli += ' (default: {0})'.format("yes" if question_default == 1 else "no")
|
||||
else:
|
||||
text_for_user_input_in_cli += ' (default: {0})'.format(question_default)
|
||||
|
||||
# Check for a password argument
|
||||
is_password = True if question_type == 'password' else False
|
||||
|
||||
if question_type == 'domain':
|
||||
question_default = _get_maindomain()
|
||||
text_for_user_input_in_cli += ' (default: {0})'.format(question_default)
|
||||
msignals.display(m18n.n('domains_available'))
|
||||
for domain in domain_list()['domains']:
|
||||
msignals.display("- {}".format(domain))
|
||||
|
||||
elif question_type == 'user':
|
||||
msignals.display(m18n.n('users_available'))
|
||||
for user in user_list()['users'].keys():
|
||||
msignals.display("- {}".format(user))
|
||||
|
||||
elif question_type == 'password':
|
||||
msignals.display(m18n.n('good_practices_about_user_password'))
|
||||
|
||||
try:
|
||||
input_string = msignals.prompt(text_for_user_input_in_cli, is_password)
|
||||
except NotImplementedError:
|
||||
input_string = None
|
||||
if (input_string == '' or input_string is None) \
|
||||
and question_default is not None:
|
||||
question_value = question_default
|
||||
else:
|
||||
question_value = input_string
|
||||
elif question_default is not None:
|
||||
question_value = question_default
|
||||
|
||||
# If the value is empty (none or '')
|
||||
# then check if question is optional or not
|
||||
if question_value is None or question_value == '':
|
||||
if question.get("optional", False):
|
||||
# Argument is optional, keep an empty value
|
||||
# and that's all for this question!
|
||||
parsed_answers_dict[question_name] = ('', question_type)
|
||||
continue
|
||||
else:
|
||||
# The argument is required !
|
||||
raise YunohostError('app_argument_required', name=question_name)
|
||||
|
||||
# Validate argument choice
|
||||
if question_choices and question_value not in question_choices:
|
||||
raise YunohostError('app_argument_choice_invalid', name=question_name, choices=', '.join(question_choices))
|
||||
|
||||
# Validate argument type
|
||||
if question_type == 'domain':
|
||||
if question_value not in domain_list()['domains']:
|
||||
raise YunohostError('app_argument_invalid', name=question_name, error=m18n.n('domain_unknown'))
|
||||
elif question_type == 'user':
|
||||
if question_value not in user_list()["users"].keys():
|
||||
raise YunohostError('app_argument_invalid', name=question_name, error=m18n.n('user_unknown', user=question_value))
|
||||
elif question_type == 'app':
|
||||
if not _is_installed(question_value):
|
||||
raise YunohostError('app_argument_invalid', name=question_name, error=m18n.n('app_unknown'))
|
||||
elif question_type == 'boolean':
|
||||
if isinstance(question_value, bool):
|
||||
question_value = 1 if question_value else 0
|
||||
else:
|
||||
if str(question_value).lower() in ["1", "yes", "y"]:
|
||||
question_value = 1
|
||||
elif str(question_value).lower() in ["0", "no", "n"]:
|
||||
question_value = 0
|
||||
else:
|
||||
raise YunohostError('app_argument_choice_invalid', name=question_name, choices='yes, no, y, n, 1, 0')
|
||||
elif question_type == 'password':
|
||||
forbidden_chars = "{}"
|
||||
if any(char in question_value for char in forbidden_chars):
|
||||
raise YunohostError('pattern_password_app', forbidden_chars=forbidden_chars)
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
assert_password_is_strong_enough('user', question_value)
|
||||
parsed_answers_dict[question_name] = (question_value, question_type)
|
||||
answer = parser.parse(question=question, user_answers=user_answers)
|
||||
if answer is not None:
|
||||
parsed_answers_dict[question["name"]] = answer
|
||||
|
||||
return parsed_answers_dict
|
||||
|
||||
|
|
|
@ -705,15 +705,16 @@ def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default():
|
|||
|
||||
def test_parse_args_in_yunohost_format_domain_empty():
|
||||
questions = [{"name": "some_domain", "type": "domain",}]
|
||||
main_domain = "my_main_domain.com"
|
||||
expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
|
||||
answers = {}
|
||||
|
||||
with patch.object(
|
||||
domain, "_get_maindomain", return_value="my_main_domain.com"
|
||||
), patch.object(
|
||||
domain, "domain_list", return_value={"domains": ["my_main_domain.com"]}
|
||||
domain, "domain_list", return_value={"domains": [main_domain]}
|
||||
):
|
||||
with pytest.raises(YunohostError):
|
||||
_parse_args_in_yunohost_format(answers, questions)
|
||||
assert _parse_args_in_yunohost_format(answers, questions) == expected_result
|
||||
|
||||
|
||||
def test_parse_args_in_yunohost_format_domain():
|
||||
|
|
Loading…
Add table
Reference in a new issue