[mod] refactor the whole argument parsing flow into a component oriented way

This commit is contained in:
Laurent Peuch 2020-05-26 06:03:49 +02:00
parent 0aebd575f7
commit 05aa54ac0f
2 changed files with 186 additions and 114 deletions

View file

@ -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

View file

@ -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():