1"""
2Digress's CLI interface.
3"""
4
5import inspect
6import sys
7from optparse import OptionParser
8
9import textwrap
10
11from types import MethodType
12
13from digress import __version__ as version
14
15def dispatchable(func):
16    """
17    Mark a method as dispatchable.
18    """
19    func.digress_dispatchable = True
20    return func
21
22class Dispatcher(object):
23    """
24    Dispatcher for CLI commands.
25    """
26    def __init__(self, fixture):
27        self.fixture = fixture
28        fixture.dispatcher = self
29
30    def _monkey_print_help(self, optparse, *args, **kwargs):
31        # monkey patches OptionParser._print_help
32        OptionParser.print_help(optparse, *args, **kwargs)
33
34        print >>sys.stderr, "\nAvailable commands:"
35
36        maxlen = max([ len(command_name) for command_name in self.commands ])
37
38        descwidth = 80 - maxlen - 4
39
40        for command_name, command_meth in self.commands.iteritems():
41            print >>sys.stderr, "  %s %s\n" % (
42                command_name.ljust(maxlen + 1),
43                ("\n" + (maxlen + 4) * " ").join(
44                    textwrap.wrap(" ".join(filter(
45                            None,
46                            command_meth.__doc__.strip().replace("\n", " ").split(" ")
47                        )),
48                        descwidth
49                    )
50                )
51            )
52
53    def _enable_flush(self):
54        self.fixture.flush_before = True
55
56    def _populate_parser(self):
57        self.commands = self._get_commands()
58
59        self.optparse = OptionParser(
60            usage = "usage: %prog [options] command [args]",
61            description = "Digress CLI frontend for %s." % self.fixture.__class__.__name__,
62            version = "Digress %s" % version
63        )
64
65        self.optparse.print_help = MethodType(self._monkey_print_help, self.optparse, OptionParser)
66
67        self.optparse.add_option(
68            "-f",
69            "--flush",
70            action="callback",
71            callback=lambda option, opt, value, parser: self._enable_flush(),
72            help="flush existing data for a revision before testing"
73        )
74
75        self.optparse.add_option(
76            "-c",
77            "--cases",
78            metavar="FOO,BAR",
79            action="callback",
80            dest="cases",
81            type=str,
82            callback=lambda option, opt, value, parser: self._select_cases(*value.split(",")),
83            help="test cases to run, run with command list to see full list"
84        )
85
86    def _select_cases(self, *cases):
87        self.fixture.cases = filter(lambda case: case.__name__ in cases, self.fixture.cases)
88
89    def _get_commands(self):
90        commands = {}
91
92        for name, member in inspect.getmembers(self.fixture):
93            if hasattr(member, "digress_dispatchable"):
94                commands[name] = member
95
96        return commands
97
98    def _run_command(self, name, *args):
99        if name not in self.commands:
100            print >>sys.stderr, "error: %s is not a valid command\n" % name
101            self.optparse.print_help()
102            return
103
104        command = self.commands[name]
105
106        argspec = inspect.getargspec(command)
107
108        max_arg_len = len(argspec.args) - 1
109        min_arg_len = max_arg_len - ((argspec.defaults is not None) and len(argspec.defaults) or 0)
110
111        if len(args) < min_arg_len:
112            print >>sys.stderr, "error: %s takes at least %d arguments\n" % (
113                name,
114                min_arg_len
115            )
116            print >>sys.stderr, "%s\n" % command.__doc__
117            self.optparse.print_help()
118            return
119
120        if len(args) > max_arg_len:
121            print >>sys.stderr, "error: %s takes at most %d arguments\n" % (
122                name,
123                max_arg_len
124            )
125            print >>sys.stderr, "%s\n" % command.__doc__
126            self.optparse.print_help()
127            return
128
129        command(*args)
130
131    def pre_dispatch(self):
132        pass
133
134    def dispatch(self):
135        self._populate_parser()
136
137        self.optparse.parse_args()
138        self.pre_dispatch()
139        args = self.optparse.parse_args()[1] # arguments may require reparsing after pre_dispatch; see test_x264.py
140
141        if len(args) == 0:
142            print >>sys.stderr, "error: no comamnd specified\n"
143            self.optparse.print_help()
144            return
145
146        command = args[0]
147        addenda = args[1:]
148
149        self._run_command(command, *addenda)
150