Actions.__init__()   F
last analyzed

Complexity

Conditions 10

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 10
c 4
b 0
f 0
dl 0
loc 38
rs 3.1304

How to fix   Complexity   

Complexity

Complex classes like Actions.__init__() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
                          "some properties are already set in the XML file. "
144
                          "These would be overwritten by this operation. "
145
                          "To perform this operation anyway, add the option "
146
                          "'--force' to your command.", xh.filename)
147
148
                # set default values for the given properties
149
                for i in _set:
150
                    ret = xh.get(i)
151
                    if len(ret[i]) == 0 or self.__args.force:
152
                        xh.set({ i: str(_set[i]) })
153
154
                # if bugtracker options are provided, set default values
155
                for i in BT_ELEMENTLIST:
156
                    rprop = i.replace("/", "_")
157
158
                    if hasattr(self.__args, rprop) and \
159
                       getattr(self.__args, rprop) is not None and \
160
                       len(getattr(self.__args, rprop)) >= 1:
161
                        xh.set({ i: getattr(self.__args, rprop) })
162
            else:
163
                print("[{}] Initialized default properties for {!r}: {}. ".format(\
164
                    red(" error "),
165
                    f,
166
                    red(self.__xml[f]["errorstr"])))
167
168
        # save the changes
169
        if validfiles:
170
            for f in self.__files:
171
                if "error" not in self.__xml[f]:
172
                    self.__xml[f]["handler"].write()
173
174
        # print the statistics
175
        message = "\n"
176
        if validfiles < 0:
177
            message += ("Successfully initialized {} files. ".format(\
178
                green(validfiles)))
179
        if invalidfiles < 0:
180
            message += ("{} files failed.".format(\
181
                red(invalidfiles)))
182
183
    def set(self, arguments):
184
        """Set key/value pairs from arguments
185
186
        :param list arguments: List of arguments with key=value pairs
187
        """
188
        logmgr_flog()
189
190
        # count all valid and invalid xml files
191
        validfiles, invalidfiles = self.get_files_status(self.__xml)
192
193
        # split key and value
194
        args = [i.split("=") for i in arguments]
195
196
        # iter through all key and values
197
        for f in self.__files:
198
            if "error" in self.__xml[f]:
199
                print("[ {} ] {} -> {}".format(red("error"), f, red(self.__xml[f]['errorstr'])))
200
            else:
201
                for arg in args:
202
                    try:
203
                        key, value = arg
204
205
                        if key == "languages":
206
                            value = value.split(",")
207
                            value = ",".join(self.remove_duplicate_langcodes(value))
208
209
                        log.debug("[%s] Trying to set value for property "
210
                                  "%r to %r.", f, key, value)
211
212
                        if self.__args.bugtracker:
213
                            self.__xml[f]["handler"].set({"bugtracker/" + key: value})
214
                        else:
215
                            self.__xml[f]["handler"].set({key: value})
216
                    except ValueError:
217
                        log.error('Invalid usage. '
218
                                  'Set values with the following format: '
219
                                  'property=value')
220
                        sys.exit(ReturnCodes.E_INVALID_USAGE_KEYVAL)
221
222
                print("[ {} ] Set data for file {}.".format(green("ok"), f))
223
224
        # save the changes
225
        for f in self.__files:
226
            if "error" not in self.__xml[f]:
227
                log.debug("[%s] Trying to save the changes.", f)
228
                self.__xml[f]["handler"].write()
229
230
        print_stats(validfiles, invalidfiles)
231
232
233
    def set_attr(self, arguments):
234
        prop = self.__args.property
235
        attrs = self.__args.attributes
236
237
        if not prop:
238
            log.error("You must specify a property with -p!")
239
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
240
241
        if not attrs:
242
            log.error("You must specify at least one attribute with -a!")
243
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
244
245
        # count all valid and invalid xml files
246
        validfiles, invalidfiles = self.get_files_status(self.__xml)
247
248
        data = OrderedDict()
249
        for i in attrs:
250
            try:
251
                key, val = i.split("=")
252
                data[key] = val
253
            except ValueError:
254
                log.error("The values of -a must have a key and a value, like: key=value or key=")
255
                sys.exit(ReturnCodes.E_INVALID_USAGE_KEYVAL)
256
257
        for f in self.__files:
258
            if "error" in self.__xml[f]:
259
                print("[{}] {} -> {}".format(red(" error "), f, red(self.__xml[f]["errorstr"])))
260
            else:
261
                try:
262 View Code Duplication
                    self.__xml[f]["handler"].set_attr(prop, data)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
263
                    self.__xml[f]["handler"].write()
264
265
                    print("[{}] Set attributes for file {}.".format(green(" ok "), f))
266
                except DMPropertyNotFound:
267
                    print("[{}] Property {} was not found in {}.".format(red(" error "), yellow(prop), f))
268
269
                    # we must substract 1 of "validfiles" since XML files are valid even
270
                    # if they don't have the given property.
271
                    validfiles -= 1
272
                    invalidfiles += 1
273
274
        print_stats(validfiles, invalidfiles)
275
276
277
    def del_attr(self, arguments):
278
        prop = self.__args.property
279
        attrs = self.__args.attributes
280
281
        if not prop:
282
            log.error("You must specify a property with -p!")
283
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
284
285
        if not attrs:
286
            log.error("You must specify at least one attribute with -a!")
287
            sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
288
289
        # count all valid and invalid xml files
290
        validfiles, invalidfiles = self.get_files_status(self.__xml)
291
292
        for f in self.__files:
293
            if "error" in self.__xml[f]:
294
                print("[{}] {} -> {}".format(red(" error "), f, red(self.__xml[f]["errorstr"])))
295
            else:
296
                try:
297
                    errors = self.__xml[f]["handler"].del_attr(prop, attrs)
298
                    self.__xml[f]["handler"].write()
299
300
                    if errors:
301
                        print("[{}] These attributes couldn't be deleted for {}: {}".format(
302
                            yellow(" notice "), f, ", ".join(errors)
303
                        ))
304
                    else:
305
                        print("[{}] Deleted attributes for file {}.".format(green(" ok "), f))
306 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
307
                except DMPropertyNotFound:
308
                    print("[{}] Property {} was not found in {}.".format(red(" error "), yellow(prop), f))
309
310
                    # we must substract 1 of "validfiles" since XML files are valid even
311
                    # if they don't have the given property.
312
                    validfiles -= 1
313
                    invalidfiles += 1
314
315
        print_stats(validfiles,invalidfiles)
316
317
318
    def get_attr(self, arguments):
319
        props = self.__args.properties
320
        attrs = self.__args.attributes
321
322
        data = dict(data=OrderedDict(),errors=None)
323
324
        for f in self.__files:
325
            data['data'][f] = self.__xml[f]["handler"].get_attr(props, attrs)
326
327
        return data
328
329
    def get(self, arguments):
330
        """Lists all properties
331
332
        :param list arguments:
333
        :return: [(FILENAME, {PROPERTIES}), ...]
334
        :rtype: list
335
        """
336
        logmgr_flog()
337
338
        output = list()
339
        errors = list()
340
341
        for f in self.__files:
342
            if "error" in self.__xml[f]:
343
                errors.append([f, self.__xml[f]['errorstr']])
344
            else:
345
                output.append((f, self.__xml[f]["handler"].get(arguments)))
346
347
        return {'data': output, 'errors': errors}
348
349
350
    def delete(self, arguments):
351
        """Delete a property
352
353
        :param list arguments:
354
        """
355
        logmgr_flog()
356
357
        # statistics variables
358
        file_errors = 0
359
        props_failed = 0
360
        props_deleted = 0
361
362
        # delete the properties
363
        for f in self.__files:
364
            if "error" in self.__xml[f]:
365
                print("[{}] {} -> {}".format(red(" error "), f, red(self.__xml[f]["errorstr"])))
366
                file_errors += 1
367
            else:
368
                failed_properties = list()
369
370
                for arg in arguments:
371
                    cond = None
372
                    prop = arg
373
                    pos = arg.find("=")
374
375
                    # look if there is condition
376
                    if pos != -1:
377
                        prop = arg[:pos]
378
                        cond = arg[pos+1:]
379
380
                    if not self.__xml[f]["handler"].delete(prop, cond):
381
                        failed_properties.append(arg)
382
                        props_failed += 1
383
                    else:
384
                        props_deleted += 1
385
386
                if not failed_properties:
387
                    print("[{}] {}".format(green(" ok "), f))
388
                else:
389
                    print("[{}] {} -> Couldn't delete these properties: {}".format(
390
                          yellow(" info "), f, ", ".join(failed_properties)
391
                         ))
392
393
        # save changes
394
        for f in self.__files:
395
            if "error" not in self.__xml[f]:
396
                self.__xml[f]["handler"].write()
397
398
        # print statistics
399
        message = "\n"
400
        if props_deleted < 0:
401
            message += "Successfully deleted {} propert{}. ".format(
402
                    green(props_deleted), 'ies' if props_deleted != 1 else 'y'
403
                )
404
        if props_failed < 0:
405
            message += "{} propert{} could not be deleted. ".format(
406
                    yellow(props_failed), 'ies' if props_failed != 1 else 'y'
407
                )
408
        if file_errors < 0:
409
            message += "{} {} invalid.".format(
410
                   red(file_errors),
411
                  'files were' if file_errors != 1 else 'file was'
412
                )
413
        print(message)
414
415
    def analyze(self, arguments): # pylint:disable=unused-argument
416
        handlers = dict()
417
418
        # Set default query format
419
        try:
420
            qformat = self.args.config['analzye']['queryformat']
421
        except KeyError:
422
            pass
423
424
        if self.args.queryformat:
425
            qformat = self.args.queryformat
426
427
        file_data = list()
428
        errors = list()
429
        ntfiledata = namedtuple("FileData", "file,out_formatted,data")
430
        validfiles, invalidfiles = self.get_files_status(self.__xml)
431
432
        for f in self.__files:
433
            if "error" in self.__xml[f]:
434
                errors.append("Error in '{}': {}".format(f, red(self.__xml[f]["errorstr"])))
435
            else:
436
                try:
437
                    analyzer = Analyzer(self.__xml[f]["handler"])
438
                except DMInvalidXMLHandlerObject:
439
                    log.critical("XML Handler object is None.")
440
441
                out = qformat[:]
442
                out = analyzer.replace_constants(out)
443
                fields = analyzer.extract_fields(out)
444
                data = analyzer.fetch_data(self.__args.filter, self.__args.sort, self.__args.default_output)
445
446
                if not self.__args.sort:
447
                    # we can print all caught data here. If we have no data, we assume that the user
448
                    # didn't want to see any data from the XML files and he just want to see the
449
                    # output of the constants like {os.file} - https://github.com/openSUSE/docmanager/issues/93
450
                    if data:
451
                        print(analyzer.format_output(out, data))
452
                    elif analyzer.filters_matched:
453
                        print(analyzer.format_output(out, data))
454
                else:
455
                    file_data.append(ntfiledata(file=f, out_formatted=out, data=data))
456
457
        if self.__args.sort:
458
            values = None
459
460
            if self.__args.sort == 'filename':
461
                values = sorted(file_data, key=lambda x: x.file)
462
            else:
463
                try:
464
                    values = sorted(file_data, key=lambda x: int(x.data[self.__args.sort]) \
465
                        if x.data[self.__args.sort].isnumeric() \
466
                        else \
467
                        x.data[self.__args.sort])
468
                except KeyError:
469
                    log.error("Could not find key '{}' in -qf for sort.")
470
471
            if values:
472
                for i in values:
473
                    print(analyzer.format_output(i.out_formatted, i.data))
474
475
        if not self.__args.quiet:
476
            print("\nSuccessfully analyzed {} XML files.".format(green(validfiles)))
477
478
        if errors and not self.__args.quiet:
479
            print("Got {} errors in the analyzed files:\n".format(red(len(errors))))
480
            for i in errors:
481
                print(i)
482
483
    def _readconfig(self, confname):
484
        """Read the configuration file
485
486
        :param str confname: name of configuration file
487
        :return: ConfigParser object
488
        """
489
490
        # exit if the config file is a directory
491
        if os.path.isdir(confname):
492
            log.error("File '{}' is a directory. Cannot write "
493
                      "into directories!".format(confname))
494
            sys.exit(ReturnCodes.E_FILE_IS_DIRECTORY)
495
496
        # open the config file with the ConfigParser
497
        conf = ConfigParser()
498
        if not conf.read(confname):
499
            if os.path.exists(confname):
500
                log.error("Permission denied for file '{}'! "
501
                          "Maybe you need sudo rights?".format(confname))
502
                sys.exit(ReturnCodes.E_PERMISSION_DENIED)
503
        return conf
504
505
    def config(self, values): # pylint:disable=unused-argument
506
        if not self.__args.system and not self.__args.user and not self.__args.repo and not self.__args.own:
507
            log.error("No config file specified. Please choice between either '--system', '--user', '--repo', or '--own'.")
508
            sys.exit(ReturnCodes.E_CONFIGCMD_NO_METHOD_SPECIFIED)
509
510
        prop = self.__args.property
511
        value = self.__args.value
512
513
        # search for the section, the property and the value
514
        pos = prop.find(".")
515
        if pos == -1:
516
            log.error("Invalid property syntax. Use: section.property")
517
            sys.exit(ReturnCodes.E_INVALID_CONFIG_PROPERTY_SYNTAX)
518
519
        section = prop[:pos]
520
        prop = prop[pos+1:]
521
522
        confname = None
523
524
        # determine config file
525
        if self.__args.system:
526
            confname = GLOBAL_CONFIG[0]
527
        elif self.__args.user:
528
            confname = USER_CONFIG
529
        elif self.__args.repo:
530
            confname = GIT_CONFIG
531
        elif self.__args.own:
532
            confname = self.__args.own
533
534
        # open the config file with the ConfigParser
535
        conf = self._readconfig(confname)
536
537
        # handle the 'get' method
538
        if value is None:
539
            if conf.has_section(section):
540
                try:
541
                    print(conf.get(section, prop))
542
                except NoOptionError:
543
                    pass
544
545
            sys.exit(ReturnCodes.E_OK)
546
547
        # add the section if its not available
548
        if not conf.has_section(section):
549
            conf.add_section(section)
550
551
        # set the property
552
        conf.set(section, prop, value)
553
554
        # save the changes
555
        try:
556
            if not os.path.exists(confname):
557
                # 'x' for creating and writing to a new file
558
                conf.write(open(confname, 'x')) # pylint:disable=bad-open-mode
559
            else:
560
                conf.write(open(confname, 'w'))
561
        except PermissionError: # pylint:disable=undefined-variable
562
            log.error("Permission denied for file '{}'! "
563
                      "Maybe you need sudo rights?".format(confname))
564
            sys.exit(ReturnCodes.E_PERMISSION_DENIED)
565
566
    def alias(self, values):
567
        action = self.__args.alias_action
568
        alias = self.__args.alias
569
        value = self.__args.command
570
        m = { 0: None, 1: GLOBAL_CONFIG[0], 2: USER_CONFIG, 3: GIT_CONFIG }
571
        configname = m.get(self.__args.method, self.__args.own)
572
        save = False
573
574
        if action != 'list':
575
            if alias is None and value is None:
576
                log.error("You have to provide an alias name for method '{}'.".format(action))
577
                sys.exit(ReturnCodes.E_INVALID_ARGUMENTS)
578
579
        if not value:
580
            value = ""
581
582
        # parse the config file
583
        conf = self._readconfig(configname)
584
585
        # add alias section if it's not found
586
        if not conf.has_section("alias"):
587
            conf.add_section("alias")
588
589
        # handle actions
590
        if action == "set":
591
            conf.set("alias", alias, value)
592
            save = True
593
        elif action == "get":
594
            try:
595
                print(conf.get("alias", alias))
596
            except NoOptionError:
597
                pass
598
        elif action == "del":
599
            save = True
600
            conf.remove_option("alias", alias)
601
        elif action == "list":
602
            data = dict()
603
            data["configfile"] = configname
604
            data["aliases"] = conf['alias']
605
606
            return data
607
608
        # save the changes
609
        if save:
610
            try:
611
                if not os.path.exists(configname):
612
                    log.error("The config file does not exists.")
613
                    sys.exit(ReturnCodes.E_FILE_NOT_FOUND)
614
615
                conf.write(open(configname, 'w'))
616
            except PermissionError:
617
                log.error("Permission denied for file '{}'! "
618
                          "Maybe you need sudo rights?".format(configname))
619
                sys.exit(ReturnCodes.E_PERMISSION_DENIED)
620
621
    def remove_duplicate_langcodes(self, values):
622
        new_list = []
623
        for i in values:
624
            if i not in new_list:
625
                new_list.append(i)
626
627
        return new_list
628
629
630
    def get_files_status(self, handlers):
631
        """Count all valid and invalid XML files
632
633
        :param dict handlers: The self.__xml object with all XML handlers
634
        """
635
        validfiles = 0
636
        invalidfiles = 0
637
638
        for i in self.__files:
639
            if "error" in handlers[i]:
640
                invalidfiles += 1
641
            else:
642
                validfiles += 1
643
644
        return [validfiles, invalidfiles]
645
646
    @property
647
    def args(self):
648
        return self.__args
649