Merge pull request #143 from YunoHost/support-subcategories

[enh] Support subcategories
This commit is contained in:
Laurent Peuch 2017-08-05 11:48:22 +02:00 committed by GitHub
commit 5bf2f46f47
4 changed files with 144 additions and 12 deletions

View file

@ -456,8 +456,16 @@ class ActionsMap(object):
return arguments.get(TO_RETURN_PROP) return arguments.get(TO_RETURN_PROP)
# Retrieve action information # Retrieve action information
namespace, category, action = tid if len(tid) == 4:
func_name = '%s_%s' % (category, action.replace('-', '_')) namespace, category, subcategory, action = tid
func_name = '%s_%s_%s' % (category, subcategory, action.replace('-', '_'))
full_action_name = "%s.%s.%s.%s" % (namespace, category, subcategory, action)
else:
assert len(tid) == 3
namespace, category, action = tid
subcategory = None
func_name = '%s_%s' % (category, action.replace('-', '_'))
full_action_name = "%s.%s.%s" % (namespace, category, action)
# Lock the moulinette for the namespace # Lock the moulinette for the namespace
with MoulinetteLock(namespace, timeout): with MoulinetteLock(namespace, timeout):
@ -467,18 +475,18 @@ class ActionsMap(object):
fromlist=[func_name]) fromlist=[func_name])
func = getattr(mod, func_name) func = getattr(mod, func_name)
except (AttributeError, ImportError): except (AttributeError, ImportError):
logger.exception("unable to load function %s.%s.%s", logger.exception("unable to load function %s.%s",
namespace, category, func_name) namespace, func_name)
raise MoulinetteError(errno.EIO, m18n.g('error_see_log')) raise MoulinetteError(errno.EIO, m18n.g('error_see_log'))
else: else:
log_id = start_action_logging() log_id = start_action_logging()
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
# Log arguments in debug mode only for safety reasons # Log arguments in debug mode only for safety reasons
logger.info('processing action [%s]: %s.%s.%s with args=%s', logger.info('processing action [%s]: %s with args=%s',
log_id, namespace, category, action, arguments) log_id, full_action_name, arguments)
else: else:
logger.info('processing action [%s]: %s.%s.%s', logger.info('processing action [%s]: %s',
log_id, namespace, category, action) log_id, full_action_name)
# Load translation and process the action # Load translation and process the action
m18n.load_namespace(namespace) m18n.load_namespace(namespace)
@ -595,7 +603,16 @@ class ActionsMap(object):
# category_name is stuff like "user", "domain", "hooks"... # category_name is stuff like "user", "domain", "hooks"...
# category_values is the values of this category (like actions) # category_values is the values of this category (like actions)
for category_name, category_values in actionsmap.items(): for category_name, category_values in actionsmap.items():
actions = category_values.pop('actions')
if "actions" in category_values:
actions = category_values.pop('actions')
else:
actions = {}
if "subcategories" in category_values:
subcategories = category_values.pop('subcategories')
else:
subcategories = {}
# Get category parser # Get category parser
category_parser = top_parser.add_category_parser(category_name, category_parser = top_parser.add_category_parser(category_name,
@ -625,4 +642,36 @@ class ActionsMap(object):
if 'configuration' in action_options: if 'configuration' in action_options:
category_parser.set_conf(tid, action_options['configuration']) category_parser.set_conf(tid, action_options['configuration'])
# subcategory_name is like "cert" in "domain cert status"
# subcategory_values is the values of this subcategory (like actions)
for subcategory_name, subcategory_values in subcategories.items():
actions = subcategory_values.pop('actions')
# Get subcategory parser
subcategory_parser = category_parser.add_subcategory_parser(subcategory_name, **subcategory_values)
# action_name is like "status" of "domain cert status"
# action_options are the values
for action_name, action_options in actions.items():
arguments = action_options.pop('arguments', {})
tid = (namespace, category_name, subcategory_name, action_name)
try:
# Get action parser
action_parser = subcategory_parser.add_action_parser(action_name, tid, **action_options)
except AttributeError:
# No parser for the action
continue
# Store action identifier and add arguments
action_parser.set_defaults(_tid=tid)
action_parser.add_arguments(arguments,
extraparser=self.extraparser,
format_arg_names=top_parser.format_arg_names,
validate_extra=validate_extra)
if 'configuration' in action_options:
category_parser.set_conf(tid, action_options['configuration'])
return top_parser return top_parser

View file

@ -5,7 +5,8 @@ import os
import errno import errno
import logging import logging
import argparse import argparse
from collections import deque import copy
from collections import deque, OrderedDict
from moulinette import msignals, msettings, m18n from moulinette import msignals, msettings, m18n
from moulinette.core import (init_authenticator, MoulinetteError) from moulinette.core import (init_authenticator, MoulinetteError)
@ -461,7 +462,7 @@ class _ExtendedSubParsersAction(argparse._SubParsersAction):
self.required = required self.required = required
self._deprecated_command_map = {} self._deprecated_command_map = {}
def add_parser(self, name, **kwargs): def add_parser(self, name, type_=None, **kwargs):
deprecated = kwargs.pop('deprecated', False) deprecated = kwargs.pop('deprecated', False)
deprecated_alias = kwargs.pop('deprecated_alias', []) deprecated_alias = kwargs.pop('deprecated_alias', [])
@ -478,6 +479,8 @@ class _ExtendedSubParsersAction(argparse._SubParsersAction):
self._deprecated_command_map[command] = name self._deprecated_command_map[command] = name
self._name_parser_map[command] = parser self._name_parser_map[command] = parser
parser.type = type_
return parser return parser
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
@ -576,6 +579,63 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
action, arg_strings) action, arg_strings)
return value return value
# Adapted from :
# https://github.com/python/cpython/blob/af26c15110b76195e62a06d17e39176d42c0511c/Lib/argparse.py#L2293-L2314
def format_help(self):
formatter = self._get_formatter()
# usage
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
# description
formatter.add_text(self.description)
# positionals, optionals and user-defined groups
for action_group in self._action_groups:
# Dirty hack to separate 'subcommands'
# into 'actions' and 'subcategories'
if action_group.title == "subcommands":
# Make a copy of the "action group actions"...
choice_actions = action_group._group_actions[0]._choices_actions
actions_subparser = copy.copy(action_group._group_actions[0])
subcategories_subparser = copy.copy(action_group._group_actions[0])
# Filter "action"-type and "subcategory"-type commands
actions_subparser.choices = OrderedDict([(k,v) for k,v in actions_subparser.choices.items() if v.type == "action"])
subcategories_subparser.choices = OrderedDict([(k,v) for k,v in subcategories_subparser.choices.items() if v.type == "subcategory"])
actions_choices = actions_subparser.choices.keys()
subcategories_choices = subcategories_subparser.choices.keys()
actions_subparser._choices_actions = [ c for c in choice_actions if c.dest in actions_choices ]
subcategories_subparser._choices_actions = [ c for c in choice_actions if c.dest in subcategories_choices ]
# Display each section (actions and subcategories)
if actions_choices != []:
formatter.start_section("actions")
formatter.add_arguments([actions_subparser])
formatter.end_section()
if subcategories_choices != []:
formatter.start_section("subcategories")
formatter.add_arguments([subcategories_subparser])
formatter.end_section()
else:
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
# epilog
formatter.add_text(self.epilog)
# determine help from format above
return formatter.format_help()
# This is copy-pasta from the original argparse.HelpFormatter : # This is copy-pasta from the original argparse.HelpFormatter :
# https://github.com/python/cpython/blob/1e73dbbc29c96d0739ffef92db36f63aa1aa30da/Lib/argparse.py#L293-L383 # https://github.com/python/cpython/blob/1e73dbbc29c96d0739ffef92db36f63aa1aa30da/Lib/argparse.py#L293-L383

View file

@ -579,6 +579,9 @@ class ActionsMapParser(BaseActionsMapParser):
def add_category_parser(self, name, **kwargs): def add_category_parser(self, name, **kwargs):
return self return self
def add_subcategory_parser(self, name, **kwargs):
return self
def add_action_parser(self, name, tid, api=None, **kwargs): def add_action_parser(self, name, tid, api=None, **kwargs):
"""Add a parser for an action """Add a parser for an action

View file

@ -268,6 +268,24 @@ class ActionsMapParser(BaseActionsMapParser):
""" """
parser = self._subparsers.add_parser(name, help=category_help, **kwargs) parser = self._subparsers.add_parser(name, help=category_help, **kwargs)
return self.__class__(self, parser, {
'title': "subcommands", 'required': True
})
def add_subcategory_parser(self, name, subcategory_help=None, **kwargs):
"""Add a parser for a subcategory
Keyword arguments:
- subcategory_help -- A brief description for the category
Returns:
A new ActionsMapParser object for the category
"""
parser = self._subparsers.add_parser(name,
type_="subcategory",
help=subcategory_help,
**kwargs)
return self.__class__(self, parser, { return self.__class__(self, parser, {
'title': "actions", 'required': True 'title': "actions", 'required': True
}) })
@ -285,7 +303,9 @@ class ActionsMapParser(BaseActionsMapParser):
A new ExtendedArgumentParser object for the action A new ExtendedArgumentParser object for the action
""" """
return self._subparsers.add_parser(name, help=action_help, return self._subparsers.add_parser(name,
type_="action",
help=action_help,
deprecated=deprecated, deprecated=deprecated,
deprecated_alias=deprecated_alias) deprecated_alias=deprecated_alias)