Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / utils.py @ f997679d

History | View | Annotate | Download (11.4 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33
from .errors import CLIUnknownCommand, CLISyntaxError, CLICmdSpecError
34

    
35
class Argument(Object):
36
    """An argument that can be parsed from command line or otherwise"""
37

    
38
    def __init__(self, name, arity, help=None, parsed_name=None):
39
        self.name = name
40
        self.arity = int(arity)
41

    
42
        if help is not None:
43
            self.help = help
44
        if parsed_name is not None:
45
            self.parsed_name = parsed_name
46

    
47
    @property 
48
    def name(self):
49
        return getattr(self, '_name', None)
50
    @name.setter
51
    def name(self, newname):
52
        self._name = unicode(newname)
53

    
54
    @property 
55
    def parsed_name(self):
56
        return getattr(self, '_parsed_name', None)
57
    @parsed_name.setter
58
    def parsed_name(self, newname):
59
        self._parsed_name = getattr(self, '_parsed_name', [])
60
        if isinstance(newname, list):
61
            self._parsed_name += newname
62
        else:
63
            self._parsed_name.append(unicode(newname))
64

    
65
    @property 
66
    def help(self):
67
        return getattr(self, '_help', None)
68
    @help.setter
69
    def help(self, newhelp):
70
        self._help = unicode(newhelp)
71

    
72
    @property 
73
    def arity(self):
74
        return getattr(self, '_arity', None)
75
    @arity.setter
76
    def arity(self, newarity):
77
        newarity = int(newarity)
78
        assert newarity >= 0
79
        self._arity = newarity
80

    
81
    @property 
82
    def default(self):
83
        if not hasattr(self, '_default'):
84
            self._default = False if self.arity == 0 else None
85
        return self._default
86
    @default.setter
87
    def default(self, newdefault):
88
        self._default = newdefault
89

    
90
    @property 
91
    def value(self):
92
        return getattr(self, '_value', self.default)
93
    @value.setter
94
    def value(self, newvalue):
95
        self._value = newvalue
96

    
97
    def update_parser(self, parser):
98
        """Update an argument parser with this argument info"""
99
        action = 'store_true' if self.arity == 0 else 'store'
100
        parser.add_argument(*(self.parsed_name), dest=self.name, action=action,
101
            default=self.default, help=self.help)
102

    
103
    @classmethod
104
    def test(self):
105
        h = Argument('heelp', 0, help='Display a help massage', parsed_name=['--help', '-h'])
106
        b = Argument('bbb', 1, help='This is a bbb', parsed_name='--bbb')
107
        c = Argument('ccc', 3, help='This is a ccc', parsed_name='--ccc')
108

    
109
        from argparse import ArgumentParser
110
        parser = ArgumentParser(add_help=False)
111
        h.update_parser(parser)
112
        b.update_parser(parser)
113
        c.update_parser(parser)
114

    
115
        args, argv = parser.parse_known_args()
116
        print('args: %s\nargv: %s'%(args, argv))
117

    
118
class CommandTree(Object):
119
    """A tree of command terms usefull for fast commands checking
120
    None key is used to denote that its parent is a terminal symbol
121
    and also the command spec class
122
    e.g. add(store_list_all) will result to this:
123
        {'store': {
124
            'list': {
125
                'all': {
126
                    '_spec':<store_list_all class>
127
                }
128
            }
129
        }
130
    then add(store_list) and store_info will create this:
131
        {'store': {
132
            'list': {
133
                None: <store_list class>
134
                'all': {
135
                    None: <store_list_all class>
136
                },
137
            'info': {
138
                None: <store_info class>
139
                }
140
            }
141
        }
142
    """
143

    
144
    cmd_spec_locations = [
145
        'kamaki.cli.commands',
146
        'kamaki.commands',
147
        'kamaki.cli',
148
        'kamaki',
149
        '']
150

    
151
    def __init__(self, zero_level_commands = []):
152
        self._commands = {}
153
        for cmd in zero_level_commands:
154
            self._commands[unicode(cmd)] = None
155

    
156
    def _get_commands_from_prefix(self, prefix):
157
        next_list = get_pathlist_from_prefix(prefix)
158
        try:
159
            for cmd in prefix:
160
                next_list = next_list[unicode(cmd)]
161
        except TypeError, KeyError:
162
            error_index = prefix.index(cmd)
163
            raise CLIUnknownCommand(message='Unknown command',
164
                details='Command %s not in path %s'%(unicode(cmd), unicode(prefix[:error_index)))
165
        assert next_list is dict
166
        return next_list 
167

    
168
    def list(prefix=[]):
169
        """ List the commands after prefix
170
        @param prefix can be either cmd1_cmd2_... or ['cmd1', 'cmd2', ...]
171
        """
172
        next_list =  self._get_commands_from_prefix(prefix)
173
        try:
174
            return next_list.keys().remove(None)
175
        except ValueError:
176
            return next_list.keys()
177

    
178
    def is_full_command(self, command):
179
        """ Check if a command exists as a full/terminal command
180
        e.g. store_list is full, store is partial, stort is not existing
181
        @param command can either be a cmd1_cmd2_... str or a ['cmd1, cmd2, ...'] list
182
        @return True if this command is in this Command Tree, False otherwise
183
        @raise CLIUnknownCommand if command is unknown to this tree
184
        """
185
        next_level = self._get_commands_from_prefix(command)
186
        if None in next_level.keys():
187
            return True
188
        return False
189

    
190
    def add(command, cmd_class):
191
        path_list = get_pathlist_from_prefix(command)
192
        d = self._commands
193
        for cmd in path_list:
194
            if not d.has_key(cmd):
195
                d[cmd] = {}
196
            d = d[cmd]
197
        d[None] = cmd_class #make it terminal
198

    
199
    def load_spec_package(self, spec_package):
200
        loaded = False
201
        for location in self.cmd_spec_locations:
202
            location += spec_package if location == '' else '.%s'%spec_package
203
            try:
204
                __import__(location) #a class decorator will put evetyrhing in place
205
                loaded = True
206
                break
207
            except ImportError:
208
                pass
209
        if not loaded:
210
            raise CLICmdSpecError(message='Cmd Spec Package %s load failed'%spec_package)
211

    
212
    def load_spec(self, spec_package, spec):
213
        """Load spec from a non nessecery loaded spec package"""
214

    
215
        loaded = False
216
        for location in self.cmd_spec_locations:
217
            location += spec_package if location == '' else '.%s'%spec_package
218
            try:
219
                __import__(location, fromlist=[spec])
220
                loaded = True
221
                break
222
            except ImportError:
223
                pass
224
        if not loaded
225
            raise CLICmdSpecError(message='Cmd Spec %s load failed'%spec)
226

    
227

    
228
def get_pathlist_from_prefix(prefix):
229
    return prefix if prefix is list else unicode(prefix).split('_')
230

    
231
def pretty_keys(d, delim='_', recurcive=False):
232
    """Transform keys of a dict from the form
233
    str1_str2_..._strN to the form strN
234
    where _ is the delimeter
235
    """
236
    new_d = {}
237
    for key, val in d.items():
238
        new_key = key.split(delim)[-1]
239
        if recurcive and isinstance(val, dict):
240
            new_val = pretty_keys(val, delim, recurcive) 
241
        else:
242
            new_val = val
243
        new_d[new_key] = new_val
244
    return new_d
245

    
246
def print_dict(d, exclude=(), ident= 0):
247
    if not isinstance(d, dict):
248
        raise CLIError(message='Cannot dict_print a non-dict object')
249
    try:
250
        margin = max(
251
            1 + max(len(unicode(key).strip()) for key in d.keys() \
252
                if not isinstance(key, dict) and not isinstance(key, list)),
253
            ident)
254
    except ValueError:
255
        margin = ident
256

    
257
    for key, val in sorted(d.items()):
258
        if key in exclude:
259
            continue
260
        print_str = '%s:' % unicode(key).strip()
261
        if isinstance(val, dict):
262
            print(print_str.rjust(margin)+' {')
263
            print_dict(val, exclude = exclude, ident = margin + 6)
264
            print '}'.rjust(margin)
265
        elif isinstance(val, list):
266
            print(print_str.rjust(margin)+' [')
267
            print_list(val, exclude = exclude, ident = margin + 6)
268
            print ']'.rjust(margin)
269
        else:
270
            print print_str.rjust(margin)+' '+unicode(val).strip()
271

    
272
def print_list(l, exclude=(), ident = 0):
273
    if not isinstance(l, list):
274
        raise CLIError(message='Cannot list_print a non-list object')
275
    try:
276
        margin = max(
277
            1 + max(len(unicode(item).strip()) for item in l \
278
                if not isinstance(item, dict) and not isinstance(item, list)),
279
            ident)
280
    except ValueError:
281
        margin = ident
282

    
283
    for item in sorted(l):
284
        if item in exclude:
285
            continue
286
        if isinstance(item, dict):
287
            print('{'.rjust(margin))
288
            print_dict(item, exclude = exclude, ident = margin + 6)
289
            print '}'.rjust(margin)
290
        elif isinstance(item, list):
291
            print '['.rjust(margin)
292
            print_list(item, exclude = exclude, ident = margin + 6)
293
            print ']'.rjust(margin)
294
        else:
295
            print unicode(item).rjust(margin)
296

    
297
def print_items(items, title=('id', 'name')):
298
    for item in items:
299
        if isinstance(item, dict) or isinstance(item, list):
300
            print ' '.join(unicode(item.pop(key)) for key in title if key in item)
301
        if isinstance(item, dict):
302
            print_dict(item)
303

    
304
def format_size(size):
305
    units = ('B', 'K', 'M', 'G', 'T')
306
    try:
307
        size = float(size)
308
    except ValueError:
309
        raise CLIError(message='Cannot format %s in bytes'%size)
310
    for unit in units:
311
        if size < 1024:
312
            break
313
        size /= 1024
314
    s = ('%.1f' % size)
315
    if '.0' == s[-2:]:
316
        s = s[:-2]
317
    return s + unit
318

    
319
def dict2file(d, f, depth = 0):
320
    for k, v in d.items():
321
        f.write('%s%s: '%('\t'*depth, k))
322
        if type(v) is dict:
323
            f.write('\n')
324
            dict2file(v, f, depth+1)
325
        elif type(v) is list:
326
            f.write('\n')
327
            list2file(v, f, depth+1)
328
        else:
329
            f.write(' %s\n'%unicode(v))
330

    
331
def list2file(l, f, depth = 1):
332
    for item in l:
333
        if type(item) is dict:
334
            dict2file(item, f, depth+1)
335
        elif type(item) is list:
336
            list2file(item, f, depth+1)
337
        else:
338
            f.write('%s%s\n'%('\t'*depth, unicode(item)))