diff --git a/hyde/command_line.py b/hyde/command_line.py index a9992bb..e3bb4a4 100644 --- a/hyde/command_line.py +++ b/hyde/command_line.py @@ -7,6 +7,7 @@ from collections import namedtuple __all__ = [ 'command', + 'subcommand', 'param', 'Application' ] @@ -15,33 +16,35 @@ class CommandLine(type): """ Meta class that enables declarative command definitions """ - def __new__(cls, name, bases, attrs): - instance = super(CommandLine, cls).__new__(cls, name, bases, attrs) + def __new__(mcs, name, bases, attrs): + instance = super(CommandLine, mcs).__new__(mcs, name, bases, attrs) subcommands = [] main_command = None for name, member in attrs.iteritems(): if hasattr(member, "command"): main_command = member - # else if member.params: - # subcommands.append(member) - parser = None + elif hasattr(member, "subcommand"): + subcommands.append(member) + main_parser = None + def add_arguments(parser, params): + """ + Adds parameters to the parser + """ + for parameter in params: + parser.add_argument(*parameter.args, **parameter.kwargs) if main_command: - parser = ArgumentParser(*main_command.command.args, **main_command.command.kwargs) - for param in main_command.params: - parser.add_argument(*param.args, **param.kwargs) + main_parser = ArgumentParser(*main_command.command.args, **main_command.command.kwargs) + add_arguments(main_parser, main_command.params) + subparsers = None + if len(subcommands): + subparsers = main_parser.add_subparsers() + for sub in subcommands: + parser = subparsers.add_parser(*sub.subcommand.args, **sub.subcommand.kwargs) + parser.set_defaults(run=sub) + add_arguments(parser, sub.params) - - # subparsers = None - # if subcommands.length: - # subparsers = parser.add_subparsers() - # - # for command in subcommands: - # - # for param in main_command.params: - # parser.add_argument(*param.args, **param.kwargs) - - instance.parser = parser - instance.main = main_command + instance.__parser__ = main_parser + instance.__main__ = main_command return instance values = namedtuple('__meta_values', 'args, kwargs') @@ -53,29 +56,39 @@ class metarator(object): def __init__(self, *args, **kwargs): self.values = values._make((args, kwargs)) - def metarate(self, f, name='values'): - setattr(f, name, self.values) - return f + def metarate(self, func, name='values'): + """ + Set the values object to the function object's namespace + """ + setattr(func, name, self.values) + return func - def __call__(self, f): - return self.metarate(f) + def __call__(self, func): + return self.metarate(func) class command(metarator): """ Used to decorate the main entry point """ - def __call__(self, f): - return self.metarate(f, name='command') + def __call__(self, func): + return self.metarate(func, name='command') + +class subcommand(metarator): + """ + Used to decorate the subcommands + """ + def __call__(self, func): + return self.metarate(func, name='subcommand') class param(metarator): """ Use this decorator instead of `ArgumentParser.add_argument`. """ - def __call__(self, f): - f.params = f.params if hasattr(f, 'params') else [] - f.params.append(self.values) - return f + def __call__(self, func): + func.params = func.params if hasattr(func, 'params') else [] + func.params.append(self.values) + return func class Application(object): @@ -86,10 +99,16 @@ class Application(object): __metaclass__ = CommandLine def parse(self, argv): - return self.parser.parse_args(argv) + """ + Simple method that delegates to the ArgumentParser + """ + return self.__parser__.parse_args(argv) def run(self, args): + """ + Runs the main command or sub command based on user input + """ if hasattr(args, 'run'): - args.run(args) + args.run(self, args) else: - self.main(args) \ No newline at end of file + self.__main__(args) \ No newline at end of file diff --git a/hyde/tests/test_command_line.py b/hyde/tests/test_command_line.py index e07323b..cf56bdc 100644 --- a/hyde/tests/test_command_line.py +++ b/hyde/tests/test_command_line.py @@ -5,32 +5,91 @@ Use nose `$ nosetests` """ -from hyde.command_line import Application, command, param -from util import trap_exit +from contextlib import nested +from hyde.command_line import Application, command, subcommand, param +from util import trap_exit_pass, trap_exit_fail from mock import Mock, patch +try: + import cStringIO as StringIO +except ImportError: + import StringIO -@trap_exit -def test_command_basic(): +import sys + +class BasicCommandLine(Application): - number_of_calls = 0 - class TestCommandLine(Application): + @command(description='test', prog='Basic') + @param('--force', action='store_true', dest='force1') + @param('--force2', action='store', dest='force2') + @param('--version', action='version', version='%(prog)s 1.0') + def main(self, params): + assert params.force1 == eval(params.force2) + self._main() - @command(description='test') - @param('--force', action='store_true', dest='force1') - @param('--force2', action='store', dest='force2') - @param('--version', action='version', version='%(prog)s 1.0') - def main(self, params): - assert params.force1 == eval(params.force2) - self._main() + def _main(): pass - def _main(): pass - with patch.object(TestCommandLine, '_main') as _main: - c = TestCommandLine() +@trap_exit_fail +def test_command_basic(): + + with patch.object(BasicCommandLine, '_main') as _main: + c = BasicCommandLine() args = c.parse(['--force', '--force2', 'True']) c.run(args) + assert _main.call_count == 1 args = c.parse(['--force2', 'False']) c.run(args) - assert _main.call_count == 2 \ No newline at end of file + assert _main.call_count == 2 + + +def test_command_version(): + with patch.object(BasicCommandLine, '_main') as _main: + c = BasicCommandLine() + exception = False + try: + c.parse(['--version']) + assert False + except SystemExit: + exception = True + assert exception + assert not _main.called + +class ComplexCommandLine(Application): + + @command(description='test', prog='Complex') + @param('--force', action='store_true', dest='force1') + @param('--force2', action='store', dest='force2') + @param('--version', action='version', version='%(prog)s 1.0') + def main(self, params): + assert params.force1 == eval(params.force2) + self._main() + + @subcommand('sub', description='test') + @param('--launch', action='store_true', dest='launch1') + @param('--launch2', action='store', dest='launch2') + def sub(self, params): + assert params.launch1 == eval(params.launch2) + self._sub() + + def _main(): pass + def _sub(): pass + + +@trap_exit_pass +def test_command_subcommands_usage(): + with nested(patch.object(ComplexCommandLine, '_main'), + patch.object(ComplexCommandLine, '_sub')) as (_main, _sub): + c = ComplexCommandLine() + c.parse(['--usage']) + +@trap_exit_fail +def test_command_subcommands(): + with nested(patch.object(ComplexCommandLine, '_main'), + patch.object(ComplexCommandLine, '_sub')) as (_main, _sub): + c = ComplexCommandLine() + args = c.parse(['sub', '--launch', '--launch2', 'True']) + c.run(args) + assert not _main.called + assert _sub.call_count == 1 \ No newline at end of file diff --git a/hyde/tests/util.py b/hyde/tests/util.py index d229f77..09b4edf 100644 --- a/hyde/tests/util.py +++ b/hyde/tests/util.py @@ -8,13 +8,21 @@ def assert_html_equals(expected, actual, sanitize=None): actual = sanitize(actual) assert expected == actual -def trap_exit(f): +def trap_exit_fail(f): def test_wrapper(*args): try: f(*args) - except SystemExit, e: - print "Error running test [%s]" % f.__name__ - print e.message - raise e + except SystemExit: + assert False + test_wrapper.__name__ = f.__name__ return test_wrapper +def trap_exit_pass(f): + def test_wrapper(*args): + try: + print f.__name__ + f(*args) + except SystemExit: + pass + test_wrapper.__name__ = f.__name__ + return test_wrapper \ No newline at end of file