-
Notifications
You must be signed in to change notification settings - Fork 0
/
commandset.py
286 lines (218 loc) · 9.12 KB
/
commandset.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""
The module :py:module:`commandset` provides an abstraction layer on
top of the standard standard python module :py:module:`argparse` to create
CLI like::
python manage.py <command> <common-parameter> ... <specific-parameter> ...
Various "commands" are supported with the same program called "command set".
Some "arguments" are common to all commands, some are specific to each command.
The whole set of commands are instances of :py:class`CommandSet` which are basically
a set of "command", each command being an instance of :py:class`Command`.
Additionaly to ``CommandSet`` and ``Command``, concepts includes "arguments"
and "parameters". Arguments and commands are not represented explicitly however:
* "arguments" are specfications of textual representations on the command line,
that is are unparsed, "concrete syntax", formal elements.
* "parameters" are the results of concrete parsing. Parameters are given to the
command to be executed via the ``do`` method. Collections of parameters
are represented as :py:class:`-.Namespace`.
"Common arguments" (or common parameters) are defined at the :py:class`CommandSet`
level (via the method :py:meth:`~.addCommonArguments`). These are available to
all commands.
By contrast "specific arguments" (or specific parameters) are defined at
:py:class`Command` level (via the method :py:meth:`~.addArguments`).
This very simple framework is intented to be used via subclassing.
See the scribesclasses.manage.py for example of use.
"""
import argparse
# import termcolor if available. Useful for colored output.
try:
import termcolor
cprint = termcolor.cprint
except:
def cprint(text,color):
print text
class CommandSet(object):
"""
A set of "Command" represented CLI like ``python manage.py <command> ...``.
Once created this set should be populated with :py:classCommand objects.
All common parameters are to be defined at once using the method addCommonArguments.
The following parameters are added in all cases:
* ``-h``, ``--help`` show an help message
* ``--csdebug`` enable command set debugging
Some class attributes can be redefined.
See https://docs.python.org/2/library/argparse.html#argumentparser-objects
"""
prog='python manage.py'
"""The way to launch the program. Default is 'python manage.py', but this could be redefined"""
usage=None
""" optional usage text of the CommandSet to change the help output """
description=None
""" optional description of the CommandSet to change the help output """
def __init__(self):
"""
Build an empty CommandSet. This command set should then
be populated with the :py:meth:`~.CommandSet.addCommand` method.
The constructor of the super-class must be called explicitely like this::
super(MyCommandSet, self).__init__()
Returns:
CommandSet: An empty command set. Commands are to be added.
"""
self.commands = []
self.mainParser = argparse.ArgumentParser(
prog=self.prog,
usage=self.usage,
description=self.description)
self.subParsers = self.mainParser.add_subparsers(
title='commands',
dest='command')
self.commonArgumentParser = argparse.ArgumentParser(add_help=False)
self.commonArgumentParser.add_argument(
'--csdebug',
action='store_true',
help='command set debugging')
self.addCommonArguments()
def addCommand(self, command):
"""
Add a command to the "command set".
A standard practice is for the client code to call this method,
once after each command creation. This could look like ::
COMMAND_SET = MyCommandSet()
COMMAND_SET.addCommand(MyCommand1())
COMMAND_SET.addCommand(MyCommand2())
# ...
Args:
command (Command): the command to be added
Returns:
None
"""
assert isinstance(command, Command)
command.commandSet = self
command.subParser = self.subParsers.add_parser(
parents=[self.commonArgumentParser],
name=command.name,
help=command.help,
description=command.description)
command.addArguments()
command.subParser.set_defaults(func=command.do)
def addCommonArguments(self):
"""
Add common arguments to all commands.
This method does nothing by default. It could be redefined by subclasses.
A common practice is to redefined it like this::
class MyCommandSet(commandset.CommandSet):
def addCommonArguments(self):
self.commonArgumentParser.add_argument( ... )
self.commonArgumentParser.add_argument( ... )
...
Returns:
None
"""
pass
def addDerivedParameters(self, parameters):
"""
Add "derived" parameters to the map of ``parameters`` given ... as parameter.
The ``parameters`` variable is modified in place.
This allows to make some check on common parameters and add
derived parameters from them.
This method does nothing by default.
It could be redefined by sublcasses.
Args:
parameters (argparse.Namespace): A collection of parameters.
Returns:
None: The ``parameters`` collection is modified in place.
"""
pass
def computeParameters(self, stringArgs):
"""
This method transforms a command line arguments (concrete syntax)
into a map of "parameters". This method will call the
py:meth:`~.CommandSet.addDerivedParameters` method.
This method is not intended to be subclassed.
it is not necessary to called it either.
This method is called bythe :py:meth:`~.CommandSet.do` command.
Args:
stringArgs (list[string]): the list of actual arguments (e.g. from the command line)
Returns:
args.Namespace: A namespace containing all parameters that can be extracted
from the command line plus some derived parameters (see the method
:py:meth:`~.CommandSet.addDerivedParameters`)
"""
parameters = self.mainParser.parse_args(stringArgs)
self.addDerivedParameters(parameters)
return parameters
def do(self, stringArgs):
"""
Execute the 'do' method of the selected command after
argument parsing.
The step are basically the following:
* the actual "arguments" are parsed. The selected command is set.
The "parameters" are set according to argument values.
* the :py:meth:`~.Command.do` method of the selected
command is executed, taking the computed
"parameters" as parameter.
:param stringArgs:
:return: Any
"""
def _p(text):
cprint(text, 'green')
def __print_parameters(args, params):
_p('#'*80)
_p( 'DEBUG: execution of command' )
_ = ' '.join(args)
_p( '-'*len(_) )
_p( _ )
_p( '-'*len(_) )
map = vars(params)
for key in map:
_p( key.ljust(10)+' = '+str(map[key]) )
_p( '='*80 )
def __print_exec_prolog():
_p( 'executing ...' )
_p( '>'*80 )
# _p( ' ' )
def __print_exec_epilog():
_p( ' ' )
_p( '<'*80 )
_p( 'execution done. Result = '+str(result) )
_p( '#'*80 )
parameters = self.computeParameters(stringArgs)
if parameters.csdebug:
__print_parameters(stringArgs, parameters)
__print_exec_prolog()
result = parameters.func(parameters)
if parameters.csdebug:
__print_exec_epilog()
return result
class Command(object):
"""
Abstract class to be subclassed by each command.
Some class attributes must/could be redefined.
"""
# See https://docs.python.org/2/library/argparse.html#argumentparser-objects
name=''
""" Must be redefined in subclasses"""
help=''
""" Could be redefined in subclasses """
description= None
""" """
def __init__(self):
"""
Dummy constructor for documentation purpose only.
It is not necessary to call this constructor.
The field are filled by the addCommand of CommandSet
"""
self.commandSet = None # This will be updated by addCommand
self.subParser = None # This will be updated by addCommand
def addArguments(self):
"""
This method must be defined for each subclass.
It should takes the following form::
self.supParser.add_argument( 'param1', ... )
self.supParser.add_argument( 'param2', ... )
See the ``ArgumentParser.add_argument`` method in the standard library
for more details :
https://docs.python.org/2/library/argparse.html#the-add-argument-method
:return: None
"""
pass
def do(self, args):
pass