Resolves Issue #205

This commit is contained in:
BJ Dierkes 2015-06-17 18:57:06 -05:00
parent 58bab70e65
commit 76d004534f
7 changed files with 718 additions and 113 deletions

View File

@ -26,9 +26,21 @@ This is a branch off of the 2.6.x stable code base. Maintenance releases for
2.6.x will happen under the stable/2.6.x git branch, while forward feature
development will happen as 2.7.x under the git master branch.
Bugs:
Bugs:
* :issue:`314` - Resolved inconsistent behavor in ``colorlog`` extension.
* :issue:`314` - Resolved inconsistent behavor in ``colorlog`` extension.
* :issue:`316` - Running Nose tests with ``-s`` option causes traceback
* :issue:`317` - CementApp.run() should return results from
``Controller._dispatch()``
Features:
* :issue:`205` - Added new ``ArgparseController`` and ``expose`` decorator
in ``ext_argparse`` to eventually replace ``CementBaseController``.
Refactoring:
* :issue:`319` - Use ``os.devnull`` instead of internal ``NullOut`` hack.
2.6.0 - Thu May 14, 2015

View File

@ -49,6 +49,18 @@ def controller_validator(klass, obj):
if type(item[1]) is not dict:
raise exc.InterfaceError(errmsg)
if not obj._meta.label == 'base' and obj._meta.stacked_on is None:
errmsg = "Controller `%s` is not stacked anywhere!" % \
obj.__class__.__name__
raise exc.InterfaceError(errmsg)
if not obj._meta.label == 'base' and \
obj._meta.stacked_type not in ['nested', 'embedded']:
raise exc.InterfaceError(
"Controller '%s' " % obj._meta.label +
"has an unknown stacked type of '%s'." %
obj._meta.stacked_type
)
class IController(interface.Interface):
@ -383,12 +395,7 @@ class CementBaseController(handler.CementBaseHandler):
metadict['aliases_only'] = contr._meta.aliases_only
metadict['controller'] = contr
commands.append(metadict)
else:
raise exc.FrameworkError(
"Controller '%s' " % contr._meta.label +
"has an unknown stacked type of '%s'." %
contr._meta.stacked_type
)
return (arguments, commands)
def _process_arguments(self):
@ -443,11 +450,11 @@ class CementBaseController(handler.CementBaseHandler):
if default_func_key in self._dispatch_map.keys():
self._dispatch_command = self._dispatch_map[default_func_key]
# def _parse_args(self):
# self.app.args.description = self._help_text
# self.app.args.usage = self._usage_text
# self.app.args.formatter_class = self._meta.argument_formatter
# self.app._parse_args()
def _parse_args(self):
self.app.args.description = self._help_text
self.app.args.usage = self._usage_text
self.app.args.formatter_class = self._meta.argument_formatter
self.app._parse_args()
def _dispatch(self):
"""
@ -470,13 +477,13 @@ class CementBaseController(handler.CementBaseHandler):
return func()
else:
self._process_arguments()
self.app._parse_args()
self._parse_args()
func = getattr(self._dispatch_command['controller'],
self._dispatch_command['func_name'])
return func()
else:
self._process_arguments()
self.app._parse_args()
self._parse_args()
@property
def _usage_text(self):

View File

@ -1,11 +1,12 @@
"""Argparse Framework Extension"""
import re
import argparse
import sys
from argparse import ArgumentParser
from argparse import ArgumentParser, SUPPRESS
from ..core import backend, arg, handler, hook
from ..core.arg import CementArgumentHandler, IArgument
from ..core.controller import IController
from ..core.exc import FrameworkError
from ..utils.misc import minimal_logger
LOG = minimal_logger(__name__)
@ -16,7 +17,7 @@ def _clean_command_label(label):
def _clean_command_func(func):
return re.sub('-', '_', func)
class ArgparseArgumentHandler(arg.CementArgumentHandler, ArgumentParser):
class ArgparseArgumentHandler(CementArgumentHandler, ArgumentParser):
"""
This class implements the :ref:`IArgument <cement.core.arg>`
@ -33,7 +34,7 @@ class ArgparseArgumentHandler(arg.CementArgumentHandler, ArgumentParser):
"""Handler meta-data."""
interface = arg.IArgument
interface = IArgument
"""The interface that this class implements."""
label = 'argparse'
@ -56,13 +57,20 @@ class ArgparseArgumentHandler(arg.CementArgumentHandler, ArgumentParser):
args = self.parse_args(arg_list)
return args
def add(self, *args, **kw):
"""
A slightly more convenient synonym for ``add_argument``.
"""
self.add_argument(*args, **kw)
def add_argument(self, *args, **kw):
"""
Add an argument to the parser. Arguments and keyword arguments are
passed directly to ArgumentParser.add_argument().
passed directly to ``ArgumentParser.add_argument()``. See the
`Argparse Documentation
<http://docs.python.org/dev/library/argparse.html>`_ for help.
"""
return super(ArgumentParser, self).add_argument(*args, **kw)
super(ArgparseArgumentHandler, self).add_argument(*args, **kw)
# FIXME: Backward compat name, will remove in Cement 3.x
@ -73,13 +81,14 @@ class ArgParseArgumentHandler(ArgparseArgumentHandler):
class expose(object):
"""
Used to expose controller functions to be listed as commands, and to
decorate the function with Meta data for the argument parser.
Used to expose functions to be listed as sub-commands under the
controller namespace. It also decorates the function with meta-data for
the argument parser.
:param hide: Whether the command should be visible.
:type hide: boolean
:param arguments: List of tuples that define arguments to add to this
command sub-parser.
commands sub-parser.
:keyword parser_options: Additional options to pass to Argparse.
:type parser_options: dict
@ -87,16 +96,16 @@ class expose(object):
.. code-block:: python
from cement.core.controller import CementBaseController, expose
from cement.ext.ext_argparse import ArgparseController, expose
class MyAppBaseController(CementBaseController):
class Base(ArgparseController):
class Meta:
label = 'base'
# Note: Default functions only work on Python 3.4+
# Note: Default functions only work in Python > 3.4
@expose(hide=True)
def default(self):
print("In MyAppBaseController.default()")
print("In Base.default()")
@expose(
help='this is the help message for my_command',
@ -107,7 +116,7 @@ class expose(object):
]
)
def my_command(self):
print("In MyAppBaseController.my_command()")
print("In Base.my_command()")
"""
# pylint: disable=W0622
@ -131,19 +140,22 @@ class expose(object):
func.__cement_meta__ = metadict
return func
# FIX ME: Should be refactored into separate BaseController and Controller
# classes in Cement 3, but that would break the interface spec in 2.x
class ArgparseController(handler.CementBaseHandler):
"""
This is an implementation of the
`IControllerHandler <#cement.core.controller.IController>`_ interface, but
as a base class that application controllers `should` subclass from.
`IController <#cement.core.controller.IController>`_ interface, but
as a base class that application controllers should subclass from.
Registering it directly as a handler is useless.
NOTE: This handler **requires** that the applications 'arg_handler' be
argparse. If using an alternative argument handler you will need to
NOTE: This handler **requires** that the applications ``arg_handler`` be
``argparse``. If using an alternative argument handler you will need to
write your own controller base class or modify this one.
NOTE: This is a re-implementation of CementBaseController. In the future,
this class will eventually replace CementBaseController.
NOTE: This is a re-implementation of
`CementBaseController <#cement.core.controller.CementBaseController>`_.
In the future, this class will eventually replace it as the default.
Usage:
@ -151,22 +163,23 @@ class ArgparseController(handler.CementBaseHandler):
from cement.ext.ext_argparse import ArgparseController
class MyAppBaseController(ArgparseController):
class Base(ArgparseController):
class Meta:
label = 'base'
description = 'MyApp is awesome'
config_defaults = dict()
arguments = []
epilog = "This is the text at the bottom of --help."
# ...
description = 'description at the top of --help'
epilog = "the text at the bottom of --help."
arguments = [
(['-f', '--foo'], dict(help='my foo option', dest='foo')),
]
class MyStackedController(ArgparseController):
class Second(ArgparseController):
class Meta:
label = 'second_controller'
aliases = ['sec', 'secondary']
label = 'second'
stacked_on = 'base'
stacked_type = 'embedded'
# ...
arguments = [
(['--foo2'], dict(help='my foo2 option', dest='foo2')),
]
"""
@ -185,7 +198,7 @@ class ArgparseController(handler.CementBaseHandler):
label = 'base'
#: A list of aliases for the controller/sub-parser. **Only available
#: in Python 3+.
#: in Python > 3**.
aliases = []
#: A config [section] to merge config_defaults into. Cement will
@ -257,7 +270,7 @@ class ArgparseController(handler.CementBaseHandler):
#: restraints in how Cement discovers and maps commands.
#:
#: Note: Currently, default function/sub-command only functions on
#: Python 3.4+. Previous versions of Python/Argparse will throw the
#: Python > 3.4. Previous versions of Python/Argparse will throw the
#: exception ``error: too few arguments``.
default_func = 'default'
@ -281,12 +294,16 @@ class ArgparseController(handler.CementBaseHandler):
self.app = app
def _setup_controllers(self):
# note this is only called on base controller, so self == 'base'
# need a list to maintain order
resolved_controllers = []
# need a dict to do key/label based lookup
resolved_controllers_map = {}
# list to maintain which controllers we haven't resolved yet
unresolved_controllers = []
for contr in handler.list('controller'):
# don't include self
# don't include self/base
if contr == self.__class__:
continue
@ -294,8 +311,9 @@ class ArgparseController(handler.CementBaseHandler):
contr._setup(self.app)
unresolved_controllers.append(contr)
# treat base/self separately
# treat self/base separately
resolved_controllers.append(self)
resolved_controllers_map['base'] = self
# all this crazy shit is to resolve controllers in the order that they
# are nested/embedded, otherwise argparse does weird things
@ -304,10 +322,15 @@ class ArgparseController(handler.CementBaseHandler):
current_parent = self._meta.label
while unresolved_controllers:
LOG.debug('unresolved controllers > %s' % unresolved_controllers)
LOG.debug('current parent > %s' % current_parent)
# handle all controllers nested on parent
current_children = []
resolved_child_controllers = []
for contr in list(unresolved_controllers):
# if stacked_on is the current parent, we want to process
# its children in this run first
if contr._meta.stacked_on == current_parent:
current_children.append(contr)
if contr._meta.stacked_type == 'embedded':
@ -315,9 +338,23 @@ class ArgparseController(handler.CementBaseHandler):
else:
resolved_child_controllers.insert(0, contr)
unresolved_controllers.remove(contr)
LOG.debug('resolve controller %s' % contr)
LOG.debug('resolved controller %s %s on %s' % \
(contr, contr._meta.stacked_type,
current_parent))
# if not, fall back on whether the stacked_on parent is
# already resolved
elif contr._meta.stacked_on in resolved_controllers_map.keys():
resolved_controllers.append(contr)
resolved_controllers_map[contr._meta.label] = contr
unresolved_controllers.remove(contr)
LOG.debug('resolved controller %s %s on %s' % \
(contr, contr._meta.stacked_type,
contr._meta.stacked_on))
resolved_controllers.extend(resolved_child_controllers)
for contr in resolved_child_controllers:
resolved_controllers_map[contr._meta.label] = contr
# then, for all those controllers... handler all controllers
# nested on them
@ -328,13 +365,17 @@ class ArgparseController(handler.CementBaseHandler):
if contr._meta.stacked_type == 'embedded':
resolved_child_controllers.append(contr)
else:
resolved_child_controllers.insert(0, contr)
unresolved_controllers.remove(contr)
LOG.debug('resolve controller %s' % contr)
LOG.debug('resolved controller %s %s on %s' % \
(contr, contr._meta.stacked_type,
child_contr._meta.label))
resolved_controllers.extend(resolved_child_controllers)
for contr in resolved_child_controllers:
resolved_controllers_map[contr._meta.label] = contr
# re-iterate with the next in line as the parent (handles multiple
# level nesting)
if unresolved_controllers:
@ -342,10 +383,7 @@ class ArgparseController(handler.CementBaseHandler):
self._controllers = resolved_controllers
# so that we can look up by controller label in self._dispatch
for contr in resolved_controllers:
self._controllers_map[contr._meta.label] = contr
self._controllers_map = resolved_controllers_map
def _process_parsed_arguments(self):
pass
@ -356,16 +394,16 @@ class ArgparseController(handler.CementBaseHandler):
if 'title' not in kwargs.keys():
kwargs['title'] = contr._meta.title
kwargs['dest'] = '__subparser__'
kwargs['dest'] = 'command'
return kwargs
def _get_parser_options(self, contr):
kwargs = contr._meta.parser_options.copy()
if sys.version_info[0] > 3:
if 'aliases' not in kwargs.keys():
kwargs['aliases'] = contr._meta.aliases
if sys.version_info[0] >= 3:
if 'aliases' not in kwargs.keys(): # pragma: nocover
kwargs['aliases'] = contr._meta.aliases # pragma: nocover
if 'description' not in kwargs.keys():
kwargs['description'] = contr._meta.description
@ -373,6 +411,8 @@ class ArgparseController(handler.CementBaseHandler):
kwargs['usage'] = contr._meta.usage
if 'epilog' not in kwargs.keys():
kwargs['epilog'] = contr._meta.epilog
if 'help' not in kwargs.keys():
kwargs['help'] = contr._meta.help
if contr._meta.hide == True:
if 'help' in kwargs.keys():
@ -405,10 +445,6 @@ class ArgparseController(handler.CementBaseHandler):
def _setup_parsers(self):
# this should only be run by the base controller
if not self._meta.label == 'base':
raise FrameworkError('Private function _setup_parsers() called'
'from non-base controller')
from cement.utils.misc import rando
_rando = rando()[:12]
@ -424,11 +460,11 @@ class ArgparseController(handler.CementBaseHandler):
parsers['base'] = self.app.args
# handle base controller separately
parsers['base'].add_argument(self._controller_option,
parsers['base'].add(self._controller_option,
action='store',
default='base',
nargs='?',
help=argparse.SUPPRESS,
help=SUPPRESS,
dest='__controller_namespace__',
)
@ -441,7 +477,7 @@ class ArgparseController(handler.CementBaseHandler):
# and if only base controller registered... go ahead and return
if len(handler.list('controller')) <= 1:
return
return # pragma: nocover
# note that the order of self._controllers was already organized by
# stacking/embedding order in self._setup_controllers ... order is
@ -468,10 +504,10 @@ class ArgparseController(handler.CementBaseHandler):
# add an invisible controller option so we can figure out what
# to call later in self._dispatch
parsers[label].add_argument(self._controller_option,
parsers[label].add(self._controller_option,
action='store',
default=contr._meta.label,
help=argparse.SUPPRESS,
help=SUPPRESS,
dest='__controller_namespace__',
)
@ -482,22 +518,16 @@ class ArgparseController(handler.CementBaseHandler):
parsers[label] = parsers[stacked_on]
def _get_parser_by_controller(self, controller):
if controller._meta.stacked_on:
if controller._meta.stacked_type == 'embedded':
parser = self._get_parser(controller._meta.stacked_on)
else:
parser = self._get_parser(controller._meta.label)
if controller._meta.stacked_type == 'embedded':
parser = self._get_parser(controller._meta.stacked_on)
else:
parser = self._get_parser(controller._meta.label)
return parser
def _get_parser_parent_by_controller(self, controller):
if controller._meta.stacked_on:
if controller._meta.stacked_type == 'embedded':
parent = self._get_parser_parent(controller._meta.stacked_on)
else:
parent = self._get_parser_parent(controller._meta.label)
if controller._meta.stacked_type == 'embedded':
parent = self._get_parser_parent(controller._meta.stacked_on)
else:
parent = self._get_parser_parent(controller._meta.label)
@ -518,12 +548,9 @@ class ArgparseController(handler.CementBaseHandler):
parser = self._get_parser_by_controller(controller)
arguments = controller._collect_arguments()
for arg, kw in arguments:
try:
LOG.debug('adding argument (args=%s, kwargs=%s)' % \
(arg, kw))
parser.add_argument(*arg, **kw)
except argparse.ArgumentError as e:
raise exc.FrameworkError(e.__str__())
LOG.debug('adding argument (args=%s, kwargs=%s)' % \
(arg, kw))
parser.add(*arg, **kw)
def _process_commands(self, controller):
label = controller._meta.label
@ -545,11 +572,11 @@ class ArgparseController(handler.CementBaseHandler):
# add an invisible dispatch option so we can figure out what to
# call later in self._dispatch
command_parser.add_argument(self._dispatch_option,
command_parser.add(self._dispatch_option,
action='store',
default="%s.%s" % (command['controller']._meta.label,
command['func_name']),
help=argparse.SUPPRESS,
help=SUPPRESS,
dest='__dispatch__',
)
@ -557,12 +584,9 @@ class ArgparseController(handler.CementBaseHandler):
LOG.debug("processing arguments for '%s' " % command['label'] + \
"command namespace")
for arg, kw in command['arguments']:
try:
LOG.debug('adding argument (args=%s, kwargs=%s)' % \
(arg, kw))
command_parser.add_argument(*arg, **kw)
except argparse.ArgumentError as e:
raise exc.FrameworkError(e.__str__())
LOG.debug('adding argument (args=%s, kwargs=%s)' % \
(arg, kw))
command_parser.add(*arg, **kw)
def _collect(self):
arguments = self._collect_arguments()
@ -615,8 +639,15 @@ class ArgparseController(handler.CementBaseHandler):
# controller with not sub-command (argparse doesn't support
# default sub-command yet... so we rely on
# __controller_namespace__ and it's default func
contr_label = self.app.pargs.__controller_namespace__
func_name = _clean_command_func(contr._meta.default_func)
# We never get here on Python < 3 as Argparse would have already
# complained about too few arguments
contr_label = self.app.pargs\
.__controller_namespace__ # pragma: nocover
contr = self._controllers_map[contr_label] # pragma: nocover
func_name = _clean_command_func( # pragma: nocover
contr._meta.default_func # pragma: nocover
) # pragma: nocover
if contr_label == 'base':
contr = self
@ -627,10 +658,15 @@ class ArgparseController(handler.CementBaseHandler):
func = getattr(contr, func_name)
return func()
else:
self.app.log.fatal(
"Controller function does not exist " + \
"%s.%s()" % (contr.__class__, func_name))
return None
# only time that we'd get here is if Controller.Meta.default_func
# is pointing to something that doesn't exist
#
# We never get here on Python < 3 as Argparse would have already
# complained about too few arguments
raise FrameworkError( # pragma: nocover
"Controller function does not exist %s.%s()" % \
(contr.__class__.__name__, func_name)) # pragma: nocover
def load(app):
handler.register(ArgparseArgumentHandler)

View File

@ -3,6 +3,23 @@
What's New
==========
New Features in Cement 2.7
--------------------------
ArgparseController
^^^^^^^^^^^^^^^^^^
Work has finally begun, and is mostly completed on the complete refactoring of
``CementBaseController``. The new ``ArgparseController`` and ``expose``
decorator from the ``argparse`` extension have the following added benefits
over ``CementBaseController``:
* FIX ME
Related:
* :issue:`205`
New Features in Cement 2.6
--------------------------

View File

@ -1,5 +1,6 @@
"""Tests for cement.core.controller."""
import re
from cement.core import exc, controller, handler
from cement.utils import test
from cement.utils.misc import rando, init_defaults
@ -21,6 +22,10 @@ class TestController(controller.CementBaseController):
def default(self):
pass
@controller.expose()
def some_command(self):
pass
class TestWithPositionalController(controller.CementBaseController):
@ -128,6 +133,25 @@ class ArgumentConflict(controller.CementBaseController):
arguments = [(['-f', '--foo'], dict())]
class Unstacked(controller.CementBaseController):
class Meta:
label = 'unstacked'
stacked_on = None
arguments = [
(['--foo6'], dict(dest='foo6')),
]
class BadStackType(controller.CementBaseController):
class Meta:
label = 'bad_stack_type'
stacked_on = 'base'
stacked_type = 'bogus_stacked_type'
arguments = [
(['--foo6'], dict(dest='foo6')),
]
class ControllerTestCase(test.CementCoreTestCase):
def test_default(self):
@ -286,3 +310,62 @@ class ControllerTestCase(test.CementCoreTestCase):
res = 'cement.ext.ext_yaml' in app.ext._loaded_extensions
self.ok(res)
@test.raises(exc.InterfaceError)
def test_invalid_stacked_on(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
handlers=[
TestController,
Unstacked,
],
)
with self.app as app:
res = app.run()
except exc.InterfaceError as e:
self.ok(re.match("(.*)is not stacked anywhere!(.*)", e.msg))
raise
@test.raises(exc.InterfaceError)
def test_invalid_stacked_type(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
handlers=[
TestController,
BadStackType,
],
)
with self.app as app:
res = app.run()
except exc.InterfaceError as e:
self.ok(re.match("(.*)has an unknown stacked type(.*)", e.msg))
raise
def test_usage_text(self):
self.reset_backend()
self.app = self.make_app(APP,
handlers=[
TestController,
],
)
with self.app as app:
self.app.controller._meta.usage = None
usage = app.controller._usage_text
self.ok(usage.startswith('%s (sub-commands ...)' % \
self.app._meta.label))
def test_help_text(self):
self.reset_backend()
self.app = self.make_app(APP,
handlers=[
TestController,
AliasesOnly,
],
)
with self.app as app:
app.run()
help = app.controller._help_text
# self.ok(usage.startswith('%s (sub-commands ...)' % \
# self.app._meta.label))

View File

@ -1,37 +1,72 @@
"""Tests for cement.ext.ext_argparse."""
import os
import sys
import re
from argparse import ArgumentError
from cement.ext.ext_argparse import ArgparseArgumentHandler
from cement.ext.ext_argparse import ArgparseController, expose
from cement.ext.ext_argparse import _clean_command_label, _clean_command_func
from cement.utils import test
from cement.utils.misc import rando, init_defaults
from cement.core import handler
from cement.core.exc import InterfaceError, FrameworkError
APP = rando()[:12]
if (sys.version_info[0] > 3 and sys.version_info[1] >= 4):
ARGPARSE_SUPPORTS_DEFAULTS = True
else:
ARGPARSE_SUPPORTS_DEFAULTS = False
class Base(ArgparseController):
class Meta:
label = 'base'
arguments = [
(['--foo'], dict(dest='foo')),
]
@expose(hide=True, help="this help doesn't get seen")
def default(self):
return "Inside Base.default"
@expose()
def cmd1(self):
return "Inside Base.cmd1"
@expose()
def command_with_dashes(self):
return "Inside Base.command_with_dashes"
class Second(ArgparseController):
class Meta:
label = 'second'
stacked_on = 'base'
stacked_type = 'embedded'
arguments = [
(['--foo2'], dict(dest='foo2')),
]
@expose()
@expose(
arguments=[
(['--cmd2-foo'],
dict(help='cmd2 sub-command only options', dest='cmd2_foo')),
]
)
def cmd2(self):
return "Inside Second.cmd2"
if self.app.pargs.cmd2_foo:
return "Inside Second.cmd2 : Foo > %s" % self.app.pargs.cmd2_foo
else:
return "Inside Second.cmd2"
class Third(ArgparseController):
class Meta:
label = 'third'
stacked_on = 'base'
stacked_type = 'nested'
arguments = [
(['--foo3'], dict(dest='foo3')),
]
@expose(hide=True)
def default(self):
@ -48,6 +83,9 @@ class Fourth(ArgparseController):
stacked_type = 'embedded'
hide = True
help = "this help doesn't get seen cause we're hiding"
arguments = [
(['--foo4'], dict(dest='foo4')),
]
@expose()
def cmd4(self):
@ -58,6 +96,11 @@ class Fifth(ArgparseController):
label = 'fifth'
stacked_on = 'third'
stacked_type = 'nested'
hide = True
help = "this help isn't seen... i'm hiding"
arguments = [
(['--foo5'], dict(dest='foo5')),
]
@expose(hide=True)
def default(self):
@ -67,6 +110,101 @@ class Fifth(ArgparseController):
def cmd5(self):
return "Inside Fifth.cmd5"
class Sixth(ArgparseController):
class Meta:
label = 'sixth'
stacked_on = 'fifth'
stacked_type = 'nested'
arguments = [
(['--foo6'], dict(dest='foo6')),
]
@expose(hide=True)
def default(self):
return "Inside Sixth.default"
@expose()
def cmd6(self):
return "Inside Sixth.cmd6"
class Seventh(ArgparseController):
class Meta:
label = 'seventh'
stacked_on = 'fourth'
stacked_type = 'embedded'
arguments = [
(['--foo7'], dict(dest='foo7')),
]
@expose()
def cmd7(self):
return "Inside Seventh.cmd7"
class Unstacked(ArgparseController):
class Meta:
label = 'unstacked'
stacked_on = None
arguments = [
(['--foo6'], dict(dest='foo6')),
]
class BadStackType(ArgparseController):
class Meta:
label = 'bad_stack_type'
stacked_on = 'base'
stacked_type = 'bogus_stacked_type'
arguments = [
(['--foo6'], dict(dest='foo6')),
]
class DuplicateArguments(ArgparseController):
class Meta:
label = 'duplicate_arguments'
arguments = [
(['--foo'], dict(dest='foo')),
]
class ControllerCommandDuplicateArguments(ArgparseController):
class Meta:
label = 'controller_command_duplicate_arguments'
@expose(
arguments = [
(['--foo'], dict(dest='foo')),
(['--foo'], dict(dest='foo')),
]
)
def sub_command(self):
pass
class AlternativeDefault(ArgparseController):
class Meta:
label = 'alternative_default'
default_func = 'alternative_default'
stacked_on = 'base'
stacked_type = 'nested'
@expose(hide=True)
def alternative_default(self):
return "Inside AlternativeDefault.alternative_default"
class BadAlternativeDefault(ArgparseController):
class Meta:
label = 'bad_alternative_default'
default_func = 'bogus_default'
stacked_on = 'base'
stacked_type = 'nested'
class Aliases(ArgparseController):
class Meta:
label = 'aliases'
aliases = ['aliases-controller', 'ac']
stacked_on = 'base'
stacked_type = 'nested'
@expose(aliases=['aliases-cmd-1', 'ac1'])
def aliases_cmd1(self):
return "Inside Aliases.aliases_cmd1"
class ArgparseExtTestCase(test.CementExtTestCase):
@ -75,46 +213,358 @@ class ArgparseExtTestCase(test.CementExtTestCase):
self.app = self.make_app(APP,
argument_handler=ArgparseArgumentHandler,
handlers=[
Sixth,
Base,
Second,
Third,
Fourth,
Fifth,
Seventh,
],
)
def test_base_controller_default(self):
def test_clean_command_label(self):
self.eq(_clean_command_label('some_cmd_name'), 'some-cmd-name')
def test_clean_command_func(self):
self.eq(_clean_command_func('some-cmd-name'), 'some_cmd_name')
def test_base_default(self):
if not ARGPARSE_SUPPORTS_DEFAULTS:
raise test.SkipTest(
'Argparse does not support default commands in Python < 3.4'
)
with self.app as app:
res = app.run()
self.eq(res, "Inside Base.default")
def test_base_cmd1(self):
with self.app as app:
app._meta.argv = ['cmd1']
res = app.run()
self.eq(res, "Inside Base.cmd1")
def test_base_command_with_dashes(self):
with self.app as app:
app._meta.argv = ['command-with-dashes']
res = app.run()
self.eq(res, "Inside Base.command_with_dashes")
def test_controller_commands(self):
with self.app as app:
app._meta.argv = ['cmd2']
res = app.run()
self.eq(res, "Inside Second.cmd2")
self.setUp()
with self.app as app:
app._meta.argv = ['third', 'cmd3']
res = app.run()
self.eq(res, "Inside Third.cmd3")
self.setUp()
with self.app as app:
app._meta.argv = ['third', 'cmd4']
res = app.run()
self.eq(res, "Inside Fourth.cmd4")
self.setUp()
with self.app as app:
app._meta.argv = ['third', 'fifth', 'cmd5']
res = app.run()
self.eq(res, "Inside Fifth.cmd5")
self.setUp()
with self.app as app:
app._meta.argv = ['third', 'fifth', 'sixth', 'cmd6']
res = app.run()
self.eq(res, "Inside Sixth.cmd6")
self.setUp()
with self.app as app:
app._meta.argv = ['third', 'cmd7']
res = app.run()
self.eq(res, "Inside Seventh.cmd7")
def test_base_cmd1_parsing(self):
with self.app as app:
app._meta.argv = ['--foo=bar', 'cmd1']
res = app.run()
self.eq(res, "Inside Base.cmd1")
self.eq(app.pargs.foo, 'bar')
def test_second_cmd2(self):
with self.app as app:
app._meta.argv = ['--foo=bar', '--foo2=bar2', 'cmd2']
res = app.run()
self.eq(res, "Inside Second.cmd2")
self.eq(app.pargs.foo, 'bar')
self.eq(app.pargs.foo2, 'bar2')
def test_third_cmd3(self):
with self.app as app:
app._meta.argv = [
'--foo=bar', '--foo2=bar2',
'third', '--foo3=bar3', '--foo4=bar4', '--foo7=bar7', 'cmd3',
]
res = app.run()
self.eq(res, "Inside Third.cmd3")
self.eq(app.pargs.foo, 'bar')
self.eq(app.pargs.foo2, 'bar2')
self.eq(app.pargs.foo3, 'bar3')
self.eq(app.pargs.foo4, 'bar4')
self.eq(app.pargs.foo7, 'bar7')
def test_fifth_cmd5(self):
with self.app as app:
app._meta.argv = [
'--foo=bar', '--foo2=bar2',
'third', '--foo3=bar3', '--foo4=bar4',
'fifth', '--foo5=bar5', 'cmd5'
]
res = app.run()
self.eq(res, "Inside Fifth.cmd5")
self.eq(app.pargs.foo, 'bar')
self.eq(app.pargs.foo2, 'bar2')
self.eq(app.pargs.foo3, 'bar3')
self.eq(app.pargs.foo4, 'bar4')
self.eq(app.pargs.foo5, 'bar5')
def test_sixth_cmd6(self):
with self.app as app:
app._meta.argv = [
'--foo=bar', '--foo2=bar2',
'third', '--foo3=bar3', '--foo4=bar4',
'fifth', '--foo5=bar5', 'sixth', '--foo6=bar6', 'cmd6',
]
res = app.run()
self.eq(res, "Inside Sixth.cmd6")
self.eq(app.pargs.foo, 'bar')
self.eq(app.pargs.foo2, 'bar2')
self.eq(app.pargs.foo3, 'bar3')
self.eq(app.pargs.foo4, 'bar4')
self.eq(app.pargs.foo5, 'bar5')
self.eq(app.pargs.foo6, 'bar6')
def test_seventh_cmd7(self):
with self.app as app:
app._meta.argv = [
'--foo=bar', '--foo2=bar2',
'third', '--foo3=bar3', '--foo4=bar4', '--foo7=bar7', 'cmd7',
]
res = app.run()
self.eq(res, "Inside Seventh.cmd7")
self.eq(app.pargs.foo, 'bar')
self.eq(app.pargs.foo2, 'bar2')
self.eq(app.pargs.foo3, 'bar3')
self.eq(app.pargs.foo4, 'bar4')
self.eq(app.pargs.foo7, 'bar7')
def test_collect(self):
with self.app as app:
args = self.app.controller._collect_arguments()
cmds = self.app.controller._collect_commands()
args2, cmds2 = self.app.controller._collect()
self.eq((args, cmds), (args2, cmds2))
def test_controller_embedded_on_base(self):
self.app._meta.argv = ['cmd2']
with self.app as app:
res = app.run()
self.eq(res, "Inside Second.cmd2")
def test_controller_nested_on_base(self):
def test_controller_command_arguments(self):
self.app._meta.argv = ['cmd2', '--cmd2-foo=bar2']
with self.app as app:
res = app.run()
self.eq(res, "Inside Second.cmd2 : Foo > bar2")
def test_controller_default_nested_on_base(self):
if not ARGPARSE_SUPPORTS_DEFAULTS:
raise test.SkipTest(
'Argparse does not support default commands in Python < 3.4'
)
self.app._meta.argv = ['third']
with self.app as app:
res = app.run()
self.eq(res, "Inside Third.default")
def test_controller_command_nested_on_base(self):
self.app._meta.argv = ['third', 'cmd3']
with self.app as app:
res = app.run()
self.eq(res, "Inside Third.cmd3")
def test_controller_doubled_embedded(self):
self.app._meta.argv = ['third', 'cmd4']
with self.app as app:
res = app.run()
self.eq(res, "Inside Fourth.cmd4")
def test_controller_double_nested(self):
def test_controller_default_double_nested(self):
if not ARGPARSE_SUPPORTS_DEFAULTS:
raise test.SkipTest(
'Argparse does not support default commands in Python < 3.4'
)
self.app._meta.argv = ['third', 'fifth']
with self.app as app:
res = app.run()
self.eq(res, "Inside Fifth.default")
self.setUp()
def test_controller_command_double_nested(self):
self.app._meta.argv = ['third', 'fifth', 'cmd5']
with self.app as app:
res = app.run()
self.eq(res, "Inside Fifth.cmd5")
self.eq(res, "Inside Fifth.cmd5")
def test_alternative_default(self):
if not ARGPARSE_SUPPORTS_DEFAULTS:
raise test.SkipTest(
'Argparse does not support default commands in Python < 3.4'
)
self.reset_backend()
self.app = self.make_app(APP,
argv=['alternative_default'],
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
AlternativeDefault,
],
)
with self.app as app:
res = app.run()
self.eq(res,
"Inside AlternativeDefault.alternative_default")
@test.raises(FrameworkError)
def test_bad_alternative_default_command(self):
if not ARGPARSE_SUPPORTS_DEFAULTS:
raise test.SkipTest(
'Argparse does not support default commands in Python < 3.4'
)
self.reset_backend()
self.app = self.make_app(APP,
argv=['bad_alternative_default'],
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
BadAlternativeDefault,
],
)
try:
with self.app as app:
res = app.run()
except FrameworkError as e:
res = re.match("(.*)does not exist(.*)bogus_default(.*)",
e.__str__())
self.ok(res)
raise
@test.raises(InterfaceError)
def test_invalid_stacked_on(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
Unstacked,
],
)
with self.app as app:
res = app.run()
except InterfaceError as e:
self.ok(re.match("(.*)is not stacked anywhere!(.*)", e.msg))
raise
@test.raises(InterfaceError)
def test_invalid_stacked_type(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
BadStackType,
],
)
with self.app as app:
res = app.run()
except InterfaceError as e:
self.ok(re.match("(.*)has an unknown stacked type(.*)", e.msg))
raise
@test.raises(ArgumentError)
def test_duplicate_arguments(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
DuplicateArguments,
],
)
with self.app as app:
res = app.run()
except ArgumentError as e:
self.ok(re.match("(.*)conflicting option string(.*)",
e.__str__()))
raise
@test.raises(ArgumentError)
def test_controller_command_duplicate_arguments(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
ControllerCommandDuplicateArguments,
],
)
with self.app as app:
res = app.run()
except ArgumentError as e:
self.ok(re.match("(.*)conflicting option string(.*)",
e.__str__()))
raise
def test_aliases(self):
if sys.version_info[0] < 3:
raise test.SkipTest(
'Argparse does not support aliases in Python < 3'
)
self.reset_backend()
self.app = self.make_app(APP,
argument_handler=ArgparseArgumentHandler,
handlers=[
Base,
Aliases,
],
)
with self.app as app:
app._meta.argv = ['aliases', 'aliases-cmd1']
res = app.run()
self.eq(res, "Inside Aliases.aliases_cmd1")
app._meta.argv = ['aliases', 'aliases-cmd-1']
app._setup_arg_handler()
res = app.run()
self.eq(res, "Inside Aliases.aliases_cmd1")
app._meta.argv = ['aliases-controller', 'aliases-cmd1']
app._setup_arg_handler()
res = app.run()
self.eq(res, "Inside Aliases.aliases_cmd1")
app._meta.argv = ['ac', 'ac1']
app._setup_arg_handler()
res = app.run()
self.eq(res, "Inside Aliases.aliases_cmd1")

View File

@ -53,8 +53,8 @@ class ShellUtilsTestCase(test.CementCoreTestCase):
self.eq(p.exitcode, 0)
def test_spawn_thread(self):
t = shell.spawn_thread(time.sleep, args=(10))
t = shell.spawn_thread(time.sleep, args=(2,))
# before joining it is alive
res = t.is_alive()
self.eq(res, True)
@ -65,7 +65,7 @@ class ShellUtilsTestCase(test.CementCoreTestCase):
res = t.is_alive()
self.eq(res, False)
t = shell.spawn_thread(time.sleep, join=True, args=(10))
t = shell.spawn_thread(time.sleep, join=True, args=(2,))
res = t.is_alive()
self.eq(res, False)