Completed
Pull Request — develop (#114)
by
unknown
01:26
created

Actions.set_attr()   D

Complexity

Conditions 8

Size

Total Lines 42

Duplication

Lines 9
Ratio 21.43 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
c 1
b 0
f 0
dl 9
loc 42
rs 4
1
#
2
# Copyright (c) 2015 SUSE Linux GmbH
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of version 3 of the GNU General Public License as
6
# published by the Free Software Foundation.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, contact SUSE LLC.
15
#
16
# To contact SUSE about this file by physical or electronic mail,
17
# you may find current contact information at www.suse.com
18
19
import os.path
20
import sys
21
import threading
22
from collections import OrderedDict, namedtuple
23
from configparser import ConfigParser, NoOptionError
24
from docmanager.analyzer import Analyzer
25
from docmanager.config import GLOBAL_CONFIG, USER_CONFIG, GIT_CONFIG
26
from docmanager.core import DEFAULT_DM_PROPERTIES, ReturnCodes, BT_ELEMENTLIST
27
from docmanager.exceptions import *
28
from docmanager.logmanager import log, logmgr_flog
29
from docmanager.shellcolors import red, green, yellow
30
from docmanager.xmlhandler import XmlHandler
31
from docmanager.display import print_stats
32
from math import trunc
33
from multiprocessing.pool import ThreadPool
34
35
36
class Actions(object):
37
    """An Actions instance represents an action event
38
    """
39
40
    def __init__(self, args):
41
        """Initialize Actions class
42
43
        :param argparse.Namespace args: result from argparse.parse_args
44
        """
45
        logmgr_flog()
46
47
        # set default variables
48
        self.__files = args.files
49
        self.__args = args
50
        self.__xml = OrderedDict()
51
52
        # set the default output format for 'alias' sub cmd to 'table'
53
        if args.action == "alias":
54
            args.format = "table"
55
56
        if self.__files:
57
            # temporary xml handler list
58
            xml = list()
59
60
            # start multiple processes for initialize all XML files
61
            with ThreadPool(processes=self.__args.jobs) as pool:
62
                for i in pool.map(self.init_xml_handlers, self.__files):
63
                    xml.append(i)
64
65
            # build the self.__xml dict
66
            for i in xml:
67
                name = i["file"]
68
                self.__xml[name] = dict()
69
70
                for x in i:
71
                    if x is not "file":
72
                        self.__xml[name][x] = i[x]
73
74
                # stop if we found an error and --stop-on-error is set
75
                if self.__args.stop_on_error and "error" in self.__xml[name]:
76
                    log.error("{}: {}".format(name, self.__xml[name]["errorstr"]))
77
                    sys.exit(self.__xml[name]["error"])
78
79
    def init_xml_handlers(self, fname):
80
        """
81
        Initializes an XmlHandler for a file.
82
83
        :param string fname: The file name
84
        """
85
        handler = None
86
87
        try:
88
            handler = { "file": fname, "handler": XmlHandler(fname, True) }
89
        except (DMXmlParseError, DMInvalidXMLRootElement, DMFileNotFoundError, DMNotDocBook5File) as err:
90
            handler = { "file": fname, "errorstr": err.errorstr, "error": err.error }
91
92
        return handler
93
94
    def parse(self):
95
        logmgr_flog()
96
97
        action = self.__args.action
98
        if hasattr(self, action) and getattr(self, action) is not None:
99
            log.debug("Action.__init__: %s", self.__args)
100
            return getattr(self, action)(self.__args.properties)
101
        else:
102
            log.error("Method \"%s\" is not implemented.", action)
103
            sys.exit(ReturnCodes.E_METHOD_NOT_IMPLEMENTED)
104
105
106
    def init(self, arguments):
107
        logmgr_flog()
108
109
        _set = dict()
110
        props = list(DEFAULT_DM_PROPERTIES)
111
112
        # count all valid and invalid xml files
113
        validfiles, invalidfiles = self.get_files_status(self.__xml)
114
115
        # append bugtracker properties if needed
116
        if self.__args.with_bugtracker:
117
            for item in BT_ELEMENTLIST:
118
                props.append(item)
119
120
        # set default properties
121
        for item in props:
122
            rprop = item.replace("/", "_")
123
124
            if hasattr(self.__args, rprop) and \
125
               getattr(self.__args, rprop) is not None:
126
                _set[item] = getattr(self.__args, rprop)
127
128
        # iter through all xml handlers and init its properties
129
        for f in self.__files:
130
            if "error" not in self.__xml[f]:
131
                xh = self.__xml[f]["handler"]
132
133
                log.info("Trying to initialize the predefined DocManager "
134
                          "properties for %r.", xh.filename)
135
136
                if xh.init_default_props(self.__args.force,
137
                                         self.__args.with_bugtracker) == 0:
138
                    print("[{}] Initialized default "
139
                          "properties for {!r}.".format(green(" ok "),
140
                                                        xh.filename))
141
                else:
142
                    log.warning("Could not initialize all properties for %r because "
143
                          "there are already some properties in the XML file "
144
                          "which would be overwritten after this operation has been "
145
                          "finished. If you want to perform this operation and "
146
                          "overwrite the existing properties, you can add the "
147
                          "'--force' option to your command.", xh.filename)
148
149
                # set default values for the given properties
150
                for i in _set:
151
                    ret = xh.get(i)
152
                    if len(ret[i]) == 0 or self.__args.force:
153
                        xh.set({ i: str(_set[i]) })
154
155
                # if bugtracker options are provided, set default values
156
                for i in BT_ELEMENTLIST:
157
                    rprop = i.replace("/", "_")
158
159
                    if hasattr(self.__args, rprop) and \
160
                       getattr(self.__args, rprop) is not None and \
161
                       len(getattr(self.__args, rprop)) >= 1:
162
                        xh.set({ i: getattr(self.__args, rprop) })
163
            else:
164
                print("[{}] Initialized default properties for {!r}: {}. ".format(\
165
                    red(" error "),
166
                    f,
167
                    red(self.__xml[f]["errorstr"])))
168
169
        # save the changes
170
        if validfiles:
171
            for f in self.__files:
172
                if "error" not in self.__xml[f]:
173
                    self.__xml[f]["handler"].write()
174
175
        # print the statistics
176
        print("\nInitialized successfully {} files. {} files failed.".format(\
177
              green(validfiles), red(invalidfiles)))
178
179
    def set(self, arguments):
180
        """Set key/value pairs from arguments
181
182
        :param list arguments: List of arguments with key=value pairs
183
        """
184
        logmgr_flog()
185
186
        # count all valid and invalid xml files
187
        validfiles, invalidfiles = self.get_files_status(self.__xml)
188
189
        # split key and value
190
        args = [i.split("=") for i in arguments]
191
192
        # iter through all key and values
193
        for f in self.__files:
194
            if "error" in self.__xml[f]:
195
                print("[ {} ] {} -> {}".format(red("error"), f, red(self.__xml[f]['errorstr'])))
196
            else:
197
                for arg in args:
198
                    try:
199
                        key, value = arg
200
201
                        if key == "languages":
202
                            value = value.split(",")
203
                            value = ",".join(self.remove_duplicate_langcodes(value))
204
205
                        log.debug("[%s] Trying to set value for property "
206
                                  "%r to %r.", f, key, value)
207
208
                        if self.__args.bugtracker:
209
                            self.__xml[f]["handler"].set({"bugtracker/" + key: value})
210
                        else:
211
                            self.__xml[f]["handler"].set({key: value})
212
                    except ValueError:
213
                        log.error('Invalid usage. '
214
                                  'Set values with the following format: '
215
                                  'property=value')
216
                        sys.exit(ReturnCodes.E_INVALID_USAGE_KEYVAL)
217
218
                print("[ {} ] Set data for file {}.".format(green("ok"), f))
219
220
        # save the changes
221
        for f in self.__files:
222
            if "error" not in self.__xml[f]:
223
                log.debug("[%s] Trying to save the changes.", f)
224
                self.__xml[f]["handler"].write()
225
226
        print_stats(validfiles, invalidfiles)
227
228
229
    def set_attr(self, arguments):
230
        prop = self.__args.property
231
        attrs = self.__args.attributes
232
233
        if not prop:
234
            log.error("You must specify a property with -p!")
235
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
236
237
        if not attrs:
238
            log.error("You must specify at least one attribute with -a!")
239
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
240
241
        # count all valid and invalid xml files
242
        validfiles, invalidfiles = self.get_files_status(self.__xml)
243
244
        data = OrderedDict()
245
        for i in attrs:
246
            try:
247
                key, val = i.split("=")
248
                data[key] = val
249
            except ValueError:
250
                log.error("The values of -a must have a key and a value, like: key=value or key=")
251
                sys.exit(ReturnCodes.E_INVALID_USAGE_KEYVAL)
252
253
        for f in self.__files:
254
            if "error" in self.__xml[f]:
255
                print("[{}] {} -> {}".format(red(" error "), f, red(self.__xml[f]["errorstr"])))
256
            else:
257
                try:
258
                    self.__xml[f]["handler"].set_attr(prop, data)
259
                    self.__xml[f]["handler"].write()
260
261
                    print("[{}] Set attributes for file {}.".format(green(" ok "), f))
262 View Code Duplication
                except DMPropertyNotFound:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
263
                    print("[{}] Property {} was not found in {}.".format(red(" error "), yellow(prop), f))
264
265
                    # we must substract 1 of "validfiles" since XML files are valid even
266
                    # if they don't have the given property.
267
                    validfiles -= 1
268
                    invalidfiles += 1
269
270
        print_stats(validfiles, invalidfiles)
271
272
273
    def del_attr(self, arguments):
274
        prop = self.__args.property
275
        attrs = self.__args.attributes
276
277
        if not prop:
278
            log.error("You must specify a property with -p!")
279
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
280
281
        if not attrs:
282
            log.error("You must specify at least one attribute with -a!")
283
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
284
285
        # count all valid and invalid xml files
286
        validfiles, invalidfiles = self.get_files_status(self.__xml)
287
288
        for f in self.__files:
289
            if "error" in self.__xml[f]:
290
                print("[{}] {} -> {}".format(red(" error "), f, red(self.__xml[f]["errorstr"])))
291
            else:
292
                try:
293
                    errors = self.__xml[f]["handler"].del_attr(prop, attrs)
294
                    self.__xml[f]["handler"].write()
295
296
                    if errors:
297
                        print("[{}] These attributes couldn't be deleted for {}: {}".format(
298
                            yellow(" notice "), f, ", ".join(errors)
299
                        ))
300
                    else:
301
                        print("[{}] Deleted attributes for file {}.".format(green(" ok "), f))
302
303
                except DMPropertyNotFound:
304
                    print("[{}] Property {} was not found in {}.".format(red(" error "), yellow(prop), f))
305
306 View Code Duplication
                    # we must substract 1 of "validfiles" since XML files are valid even
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
307
                    # if they don't have the given property.
308
                    validfiles -= 1
309
                    invalidfiles += 1
310
311
        print_stats(validfiles,invalidfiles)
312
313
314
    def get_attr(self, arguments):
315
        props = self.__args.properties
316
        attrs = self.__args.attributes
317
318
        data = dict(data=OrderedDict(),errors=None)
319
320
        for f in self.__files:
321
            data['data'][f] = self.__xml[f]["handler"].get_attr(props, attrs)
322
323
        return data
324
325
    def get(self, arguments):
326
        """Lists all properties
327
328
        :param list arguments:
329
        :return: [(FILENAME, {PROPERTIES}), ...]
330
        :rtype: list
331
        """
332
        logmgr_flog()
333
334
        output = list()
335
        errors = list()
336
337
        for f in self.__files:
338
            if "error" in self.__xml[f]:
339
                errors.append([f, self.__xml[f]['errorstr']])
340
            else:
341
                output.append((f, self.__xml[f]["handler"].get(arguments)))
342
343
        return {'data': output, 'errors': errors}
344
345
346
    def delete(self, arguments):
347
        """Delete a property
348
349
        :param list arguments:
350
        """
351
        logmgr_flog()
352
353
        # statistics variables
354
        file_errors = 0
355
        props_failed = 0
356
        props_deleted = 0
357
358
        # delete the properties
359
        for f in self.__files:
360
            if "error" in self.__xml[f]:
361
                print("[{}] {} -> {}".format(red(" error "), f, red(self.__xml[f]["errorstr"])))
362
                file_errors += 1
363
            else:
364
                failed_properties = list()
365
366
                for arg in arguments:
367
                    cond = None
368
                    prop = arg
369
                    pos = arg.find("=")
370
371
                    # look if there is condition
372
                    if pos != -1:
373
                        prop = arg[:pos]
374
                        cond = arg[pos+1:]
375
376
                    if not self.__xml[f]["handler"].delete(prop, cond):
377
                        failed_properties.append(arg)
378
                        props_failed += 1
379
                    else:
380
                        props_deleted += 1
381
382
                if not failed_properties:
383
                    print("[{}] {}".format(green(" ok "), f))
384
                else:
385
                    print("[{}] {} -> Couldn't delete these properties: {}".format(
386
                          yellow(" info "), f, ", ".join(failed_properties)
387
                         ))
388
389
        # save changes
390
        for f in self.__files:
391
            if "error" not in self.__xml[f]:
392
                self.__xml[f]["handler"].write()
393
394
        # print statistics
395
        print("")
396
        print("Deleted successfully {} propert{}, {} propert{} couldn't be deleted, and {} {} invalid.".format(
397
                green(props_deleted), 'ies' if props_deleted != 1 else 'y',
398
                yellow(props_failed), 'ies' if props_failed != 1 else 'y', red(file_errors),
399
                'files were' if file_errors != 1 else 'file was'
400
             ))
401
402
    def analyze(self, arguments): # pylint:disable=unused-argument
403
        handlers = dict()
404
405
        # Set default query format
406
        try:
407
            qformat = self.args.config['analzye']['queryformat']
408
        except KeyError:
409
            pass
410
411
        if self.args.queryformat:
412
            qformat = self.args.queryformat
413
414
        file_data = list()
415
        errors = list()
416
        ntfiledata = namedtuple("FileData", "file,out_formatted,data")
417
        validfiles, invalidfiles = self.get_files_status(self.__xml)
418
419
        for f in self.__files:
420
            if "error" in self.__xml[f]:
421
                errors.append("Error in '{}': {}".format(f, red(self.__xml[f]["errorstr"])))
422
            else:
423
                try:
424
                    analyzer = Analyzer(self.__xml[f]["handler"])
425
                except DMInvalidXMLHandlerObject:
426
                    log.critical("XML Handler object is None.")
427
428
                out = qformat[:]
429
                out = analyzer.replace_constants(out)
430
                fields = analyzer.extract_fields(out)
431
                data = analyzer.fetch_data(self.__args.filter, self.__args.sort, self.__args.default_output)
432
433
                if not self.__args.sort:
434
                    # we can print all caught data here. If we have no data, we assume that the user
435
                    # didn't want to see any data from the XML files and he just want to see the
436
                    # output of the constants like {os.file} - https://github.com/openSUSE/docmanager/issues/93
437
                    if data:
438
                        print(analyzer.format_output(out, data))
439
                    elif analyzer.filters_matched:
440
                        print(analyzer.format_output(out, data))
441
                else:
442
                    file_data.append(ntfiledata(file=f, out_formatted=out, data=data))
443
444
        if self.__args.sort:
445
            values = None
446
447
            if self.__args.sort == 'filename':
448
                values = sorted(file_data, key=lambda x: x.file)
449
            else:
450
                try:
451
                    values = sorted(file_data, key=lambda x: int(x.data[self.__args.sort]) \
452
                        if x.data[self.__args.sort].isnumeric() \
453
                        else \
454
                        x.data[self.__args.sort])
455
                except KeyError:
456
                    log.error("Could not find key '{}' in -qf for sort.")
457
458
            if values:
459
                for i in values:
460
                    print(analyzer.format_output(i.out_formatted, i.data))
461
462
        if not self.__args.quiet:
463
            print("\nSuccessfully analyzed {} XML files.".format(green(validfiles)))
464
465
        if errors and not self.__args.quiet:
466
            print("Got {} errors in the analyzed files:\n".format(red(len(errors))))
467
            for i in errors:
468
                print(i)
469
470
    def _readconfig(self, confname):
471
        """Read the configuration file
472
473
        :param str confname: name of configuration file
474
        :return: ConfigParser object
475
        """
476
477
        # exit if the config file is a directory
478
        if os.path.isdir(confname):
479
            log.error("File '{}' is a directory. Cannot write "
480
                      "into directories!".format(confname))
481
            sys.exit(ReturnCodes.E_FILE_IS_DIRECTORY)
482
483
        # open the config file with the ConfigParser
484
        conf = ConfigParser()
485
        if not conf.read(confname):
486
            if os.path.exists(confname):
487
                log.error("Permission denied for file '{}'! "
488
                          "Maybe you need sudo rights?".format(confname))
489
                sys.exit(ReturnCodes.E_PERMISSION_DENIED)
490
        return conf
491
492
    def config(self, values): # pylint:disable=unused-argument
493
        if not self.__args.system and not self.__args.user and not self.__args.repo and not self.__args.own:
494
            log.error("No config file specified. Please choice between either '--system', '--user', '--repo', or '--own'.")
495
            sys.exit(ReturnCodes.E_CONFIGCMD_NO_METHOD_SPECIFIED)
496
497
        prop = self.__args.property
498
        value = self.__args.value
499
500
        # search for the section, the property and the value
501
        pos = prop.find(".")
502
        if pos == -1:
503
            log.error("Invalid property syntax. Use: section.property")
504
            sys.exit(ReturnCodes.E_INVALID_CONFIG_PROPERTY_SYNTAX)
505
506
        section = prop[:pos]
507
        prop = prop[pos+1:]
508
509
        confname = None
510
511
        # determine config file
512
        if self.__args.system:
513
            confname = GLOBAL_CONFIG[0]
514
        elif self.__args.user:
515
            confname = USER_CONFIG
516
        elif self.__args.repo:
517
            confname = GIT_CONFIG
518
        elif self.__args.own:
519
            confname = self.__args.own
520
521
        # open the config file with the ConfigParser
522
        conf = self._readconfig(confname)
523
524
        # handle the 'get' method
525
        if value is None:
526
            if conf.has_section(section):
527
                try:
528
                    print(conf.get(section, prop))
529
                except NoOptionError:
530
                    pass
531
532
            sys.exit(ReturnCodes.E_OK)
533
534
        # add the section if its not available
535
        if not conf.has_section(section):
536
            conf.add_section(section)
537
538
        # set the property
539
        conf.set(section, prop, value)
540
541
        # save the changes
542
        try:
543
            if not os.path.exists(confname):
544
                # 'x' for creating and writing to a new file
545
                conf.write(open(confname, 'x')) # pylint:disable=bad-open-mode
546
            else:
547
                conf.write(open(confname, 'w'))
548
        except PermissionError: # pylint:disable=undefined-variable
549
            log.error("Permission denied for file '{}'! "
550
                      "Maybe you need sudo rights?".format(confname))
551
            sys.exit(ReturnCodes.E_PERMISSION_DENIED)
552
553
    def alias(self, values):
554
        action = self.__args.alias_action
555
        alias = self.__args.alias
556
        value = self.__args.command
557
        m = { 0: None, 1: GLOBAL_CONFIG[0], 2: USER_CONFIG, 3: GIT_CONFIG }
558
        configname = m.get(self.__args.method, self.__args.own)
559
        save = False
560
561
        if action != 'list':
562
            if alias is None and value is None:
563
                log.error("You have to provide an alias name for method '{}'.".format(action))
564
                sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
565
566
        if not value:
567
            value = ""
568
569
        # parse the config file
570
        conf = self._readconfig(configname)
571
572
        # add alias section if it's not found
573
        if not conf.has_section("alias"):
574
            conf.add_section("alias")
575
576
        # handle actions
577
        if action == "set":
578
            conf.set("alias", alias, value)
579
            save = True
580
        elif action == "get":
581
            try:
582
                print(conf.get("alias", alias))
583
            except NoOptionError:
584
                pass
585
        elif action == "del":
586
            save = True
587
            conf.remove_option("alias", alias)
588
        elif action == "list":
589
            data = dict()
590
            data["configfile"] = configname
591
            data["aliases"] = conf['alias']
592
593
            return data
594
595
        # save the changes
596
        if save:
597
            try:
598
                if not os.path.exists(configname):
599
                    log.error("The config file does not exists.")
600
                    sys.exit(ReturnCodes.E_FILE_NOT_FOUND)
601
602
                conf.write(open(configname, 'w'))
603
            except PermissionError:
604
                log.error("Permission denied for file '{}'! "
605
                          "Maybe you need sudo rights?".format(configname))
606
                sys.exit(ReturnCodes.E_PERMISSION_DENIED)
607
608
    def remove_duplicate_langcodes(self, values):
609
        new_list = []
610
        for i in values:
611
            if i not in new_list:
612
                new_list.append(i)
613
614
        return new_list
615
616
617
    def get_files_status(self, handlers):
618
        """Count all valid and invalid XML files
619
620
        :param dict handlers: The self.__xml object with all XML handlers
621
        """
622
        validfiles = 0
623
        invalidfiles = 0
624
625
        for i in self.__files:
626
            if "error" in handlers[i]:
627
                invalidfiles += 1
628
            else:
629
                validfiles += 1
630
631
        return [validfiles, invalidfiles]
632
633
    @property
634
    def args(self):
635
        return self.__args
636