Completed
Push — develop ( 85e641...51e497 )
by Thomas
01:23
created

Actions.set()   F

Complexity

Conditions 13

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 13
c 4
b 0
f 0
dl 0
loc 58
rs 3.1873

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

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