adding-commands.rst 12.8 KB
Newer Older
1
2
3
Adding Commands
===============

4
Kamaki commands are implemented as python classes, decorated with a special
5
6
7
8
decorator called *command*. This decorator is a method of *kamaki.cli* that
adds a new command in a *CommandTree* structure. A *CommandTree* (package
*kamaki.cli.commant_tree*) is a data structure used by kamaki to manage command
namespaces.
9

10
11
For demonstration purposes, the following set of kamaki commands will be
implemented in this document::
12
13
14
15
16
17

    mygrp1 list all                         //show a list
    mygrp1 list details [--match=<>]        //show list of details
    mygrp2 list all [regular expression] [-l]       //list all subjects
    mygrp2 info <id> [name]      //get information for subject with id

18
19
20
21
22
There are two command groups to implement i.e., *mygrp1* and *mygrp2*,
containing two commands each (*list_all*, *list_details* and *list_all*, *info*
respectively). To avoid ambiguities, command names are prefixed with the
command group they belong to, e.g., *mygrp1_list_all* and *mygrp2_list_all*.
The underscore is used to separate command namespaces.
23

24
25
26
27
The first command (*mygrp1_list_all*) has the simplest possible syntax: no
parameters, no runtime arguments. The second accepts an optional runtime argument with a value. The third features an optional parameter and an optional
runtime flag argument. The last is an example of a command with an obligatory
and an optional parameter.
28

29
Examples of the expected behavior in one-command mode:
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

.. code-block:: console

    $kamaki mygrp1
        mygrp1 description

        Options
         - - - -
        list
    $ kamaki mygrp1 list

        Options
         - - - -
        all        show a list
        details     show a list of details
    $ kamaki mygrp1 list all
46
        ... (a mygrp1_list_all instance runs) ...
47
    $ kamaki mygrp2 list all 'Z[.]' -l
48
        ... (a mygrp2_list_all instance runs) ...
49
50
51
52
53
    $

The CommandTree structure
-------------------------

54
55
56
57
58
CommandTree manages a command by its namespace. Each command is stored in
a tree path, where each node is a name. A leaf is the end term of a namespace and contains a pointer to the command class to be executed.

Here is an example from the actual kamaki command structure, where the commands
*file upload*, *file list* and *file info* are represented as shown bellow::
59

60
    - file
61
62
63
64
    ''''''''|- info
            |- list
            |- upload

65
Now, let's load the showcase example on CommandTrees::
66
67
68
69
70
71
72
73
74
75
76

    - mygrp1
    ''''''''|- list
            '''''''|- all
                   |- details

    - mygrp2
    ''''''''|- list
            '''''''|- all
            |- info

77
78
79
80
Each command group should be stored on a different CommandTree.

For that reason, command specification modules should contain a list of CommandTree objects, named *_commands*. This mechanism allows any interface
application to load the list of commands from the *_commands* array.
81

82
The first name of the command path and a description (name, description) are needed to initializeg a CommandTree:
83
84
85
86
87
88
89
90

.. code-block:: python

    _mygrp1_commands = CommandTree('mygrp', 'mygrp1 description')
    _mygrp2_commands = CommandTree('mygrp', 'mygrp2 description')

    _commands = [_mygrp1_commands, _mygrp2_commands]

91

92
93
94
The command decorator
---------------------

95
96
97
98
99
All commands are specified by subclasses of *kamaki.cli.commands._command_init*
These classes are called "command specifications".

The *command* decorator mines all the information needed to build a namespace
from a command specification::
100
101
102

    class code  --->  command()  -->  updated CommandTree structure

103
Kamaki interfaces make use of the CommandTree structure. Optimizations are
104
possible by using special parameters on the command decorator method.
105
106
107
108
109

.. code-block:: python

    def command(cmd_tree, prefix='', descedants_depth=None):
    """Load a class as a command
110

Stavros Sachtouris's avatar
Stavros Sachtouris committed
111
        :param cmd_tree: is the CommandTree to be updated with a new command
112

Stavros Sachtouris's avatar
Stavros Sachtouris committed
113
        :param prefix: of the commands allowed to be inserted ('' for all)
114

115
        :param descedants_depth: is the depth of the tree descendants of the
116
117
118
119
120
121
            prefix command.
    """

Creating a new command specification set
----------------------------------------

122
123
124
A command specification developer should create a new module (python file) with
one command specification class per command. Each class should be decorated
with *command*.
125
126
127
128
129
130
131
132
133
134
135
136

.. code-block:: python

    ...
    _commands = [_mygrp1_commands, _mygrp2_commands]

    @command(_mygrp1_commands)
    class mygrp1_list_all():
        ...

    ...

137
A list of CommandTree structures must exist in the module scope, with the name
138
139
*_commands*. Different CommandTree objects correspond to different command
groups.
140

141
Set command description
142
143
-----------------------

144
145
146
The description of each command is the first line of the class commend. The
following declaration of *mygrp2-info* command has a "*get information for
subject with id*" description.
147
148
149
150
151

.. code-block:: python

    ...
    @command(_mygrp2_commands)
152
153
154
155
156
    class mygrp2_info():
        """get information for subject with id
        Anything from this point and bellow constitutes the long description
        Please, mind the indentation, pep8 is not forgiving.
        """
157
158
        ...

159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
Description placeholders
------------------------

There is possible to create an empty command, that can act as a description
placeholder. For example, the *mygrp1_list* namespace does not correspond to an
executable command, but it can have a helpful description. In that case, create
a command specification class with a command and no code:

.. code-block:: python

    @command(_mygrp1_commands)
    class mygrp1_list():
        """List mygrp1 objects.
        There are two versions: short and detailed
        """

.. warning:: A command specification class with no description is invalid and
    will cause an error.

178
179
180
Declare run-time argument
-------------------------

181
182
183
184
185
A special argument mechanism allows the definition of run-time arguments. This
mechanism is based on argparse and is designed to simplify argument definitions
when specifying commands.

Some basic argument types are defined at the
186
`argument module <code.html#module-kamaki.cli.argument>`_, but it is not
187
188
189
a bad idea to extent these classes in order to achieve specialized type
checking and syntax control. Still, in most cases, the argument types of the
argument package are enough for most cases.
190

191
192
193
194
195
To declare a run-time argument on a specific command, the specification class
should contain a dict called *arguments* , where Argument objects are stored.
Each argument object is a run-time argument. Syntax checking happens at client
level, while the type checking is implemented in the Argument code (e.g.,
IntArgument checks if the value is an int).
196
197
198
199
200
201
202
203
204
205

.. code-block:: python

    from kamaki.cli.argument import ValueArgument
    ...

    @command(_mygrp1_commands)
    class mygrp1_list_details():
        """list of details"""

Stavros Sachtouris's avatar
Stavros Sachtouris committed
206
        def __init__(self, global_args={}):
207
208
            global_args['match'] = ValueArgument(
                'Filter results to match string',
209
                ('-m', '--match'))
210
211
            self.arguments = global_args

212
213
214
215
216
217
218
219
220
221
222
223
or more usually and elegantly:

.. code-block:: python

    from kamaki.cli.argument import ValueArgument
    
    @command(_mygrp1_commands)
    class mygrp1_list_details():
    """List of details"""

        arguments = dict(
            match=ValueArgument(
224
                'Filter output to match string', ('-m', --match'))
225
226
227
228
229
        )

Accessing run-time arguments
----------------------------

230
231
232
To access run-time arguments, users can use the *_command_init* interface,
which implements *__item__* accessors to handle run-time argument values. In
other words, one may get the value of an argument with *self[<argument>]*.
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252

.. code-block:: python

    from kamaki.cli.argument import ValueArgument
    from kamaki.cli.commands import _command_init
    
    @command(_mygrp1_commands)
    class mygrp1_list_details(_command_init):
        """List of details"""

        arguments = dict(
            match=ValueArgument(
                'Filter output to match string', ('-m', --match'))
        )

        def check_runtime_arguments(self):
            ...
            assert self['match'] == self.arguments['match'].value
            ...

253
254
255
The main method and command parameters
--------------------------------------

256
257
The command behavior for each command class is coded in *main*. The
parameters of *main* method affect the syntax of the command. In specific::
258

Stavros Sachtouris's avatar
Stavros Sachtouris committed
259
260
    main(self, param)                   - obligatory parameter <param>
    main(self, param=None)              - optional parameter [param]
261
262
263
264
265
266
267
268
    main(self, param1, param2=42)       - <param1> [param2]
    main(self, param1____param2)        - <param1:param2>
    main(self, param1____param2=[])     - [param1:param2]
    main(self, param1____param2__)      - <param1[:param2]>
    main(self, param1____param2__='')   - [param1[:param2]]
    main(self, *args)                   - arbitary number of params [...]
    main(self, param1____param2, *args) - <param1:param2> [...]

269
270
Let's have a look at the command specification class again, and highlight the
parts that affect the command syntax:
271
272
273
274
275
276
277

.. code-block:: python
    :linenos:

    from kamaki.cli.argument import FlagArgument
    ...

278
    _commands = [_mygrp1_commands, _mygrp2_commands]
279
280
281
    ...

    @command(_mygrp2_commands)
282
    class mygrp2_list_all():
283
284
285
        """List all subjects
        Refers to the subject accessible by current user
        """
286

287
        arguments = dict(FlagArgument('detailed list', '-l'))
288
289
290
291

        def main(self, reg_exp=None):
            ...

292
293
294
295
296
297
298
The above lines contain the following information:

* Namespace and name (line 8): mygrp2 list all
* Short (line 9) and long (line 10) description
* Parameters (line 15): [reg exp]
* Runtime arguments (line 13): [-l]
* Runtime arguments help (line 13): detailed list
299

300
301
302
303
.. tip:: It is suggested to code the main functionality in a member method
    called *_run*. This allows the separation between syntax and logic. For
    example, an external library may need to call a command without caring
    about its command line behavior.
304
305
306
307

Letting kamaki know
-------------------

308
309
310
Kamaki will load a command specification *only* if it is set as a configurable
option. To demonstrate this, let the command specifications coded above be
stored in a file named *grps.py*.
311

312
313
The developer should move the file *grps.py* to *kamaki/cli/commands*, the
default place for command specifications
314

315
316
These lines should be contained in the kamaki configuration file for a new
command specification module to work:
317
318
::

319
320
321
    [global]
    mygrp1_cli = grps
    mygrp2_cli = grps
322

323
or equivalently:
324
325
326

.. code-block:: console

327
328
    $ kamaki config set mygrp1_cli grps
    $ kamaki config set mygrp2_cli grps
329

330
331
332
333
.. note:: running a command specification from a different path is supported.
    To achieve this, add a *<group>_cli = </path/to/module>* line in the
    configure file under the *global* section.
::
334

335
336
    [global]
    mygrp_cli = /another/path/grps.py
337
338
339
340
341
342
343
344

Summary: create a command set
-----------------------------

.. code-block:: python

    #  File: grps.py

345
    from kamaki.cli.commands import _command_init
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
    from kamaki.cli.command_tree import CommandTree
    from kamaki.cli.argument import ValueArgument, FlagArgument
    ...


    #  Initiallize command trees

    _mygrp1_commands = CommandTree('mygrp', 'mygrp1 description')
    _mygrp2_commands = CommandTree('mygrp', 'mygrp2 description')

    _commands = [_mygrp1_commands, _mygrp2_commands]


    #  Define command specifications


362
363
364
365
366
367
368
    @command(_mygrp1_commands)
    class mygrp1_list(_command_init):
        """List mygrp1 objects.
        There are two versions: short and detailed
        """


369
    @command(_mygrp1_commands)
370
    class mygrp1_list_all(_command_init):
371
372
        """show a list"""

373
        def _run():
374
375
            ...

376
377
378
        def main(self):
            self._run()

379
380

    @command(_mygrp1_commands)
381
    class mygrp1_list_details(_command_init):
382
383
        """show list of details"""

384
385
386
387
        arguments = dict(
            match=ValueArgument(
                'Filter output to match string', ('-m', --match'))
        )
388

389
        def _run(self):
390
            match_value = self['match']
391
392
            ...

393
394
395
396
397
398
        def main(self):
        self._run()


    #The following will also create a mygrp2_list command with no description

399
400

    @command(_mygrp2_commands)
401
    class mygrp2_list_all(_command_init):
402
403
        """list all subjects"""

404
405
406
        arguments = dict(
            list=FlagArgument('detailed listing', '-l')
        )
407

408
        def _run(self, regexp):
409
            ...
410
            if self['list']:
411
                ...
412
            else:
413
                ...
414
415
416

        def main(self, regular_expression=None):
            self._run(regular_expression)
417
418
419


    @command(_mygrp2_commands)
420
    class mygrp2_info(_command_init):
421
422
        """get information for subject with id"""

423
        def _run(self, grp_id, grp_name):
424
            ...
425
426
427

        def main(self, id, name=''):
            self._run(id, name)