Test Failed
Pull Request — master (#4173)
by Arma
09:43
created

CLIConfigParser.parse()   F

Complexity

Conditions 14

Size

Total Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
dl 0
loc 75
rs 3.3381
c 0
b 0
f 0

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 CLIConfigParser.parse() 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
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
"""
17
Module for parsing CLI config file.
18
"""
19
20
from __future__ import absolute_import
21
22
import logging
23
import os
24
25
from collections import defaultdict
26
27
import io
28
29
import six
30
from six.moves.configparser import ConfigParser
31
32
__all__ = [
33
    'CLIConfigParser',
34
35
    'ST2_CONFIG_DIRECTORY',
36
    'ST2_CONFIG_PATH',
37
38
    'CONFIG_DEFAULT_VALUES'
39
]
40
41
ST2_CONFIG_DIRECTORY = '~/.st2'
42
ST2_CONFIG_DIRECTORY = os.path.abspath(os.path.expanduser(ST2_CONFIG_DIRECTORY))
43
44
ST2_CONFIG_PATH = os.path.abspath(os.path.join(ST2_CONFIG_DIRECTORY, 'config'))
45
46
CONFIG_FILE_OPTIONS = {
47
    'general': {
48
        'base_url': {
49
            'type': 'string',
50
            'default': None
51
        },
52
        'api_version': {
53
            'type': 'string',
54
            'default': None
55
        },
56
        'cacert': {
57
            'type': 'string',
58
            'default': None
59
        },
60
        'silence_ssl_warnings': {
61
            'type': 'bool',
62
            'default': False
63
        }
64
    },
65
    'cli': {
66
        'debug': {
67
            'type': 'bool',
68
            'default': False
69
        },
70
        'cache_token': {
71
            'type': 'boolean',
72
            'default': True
73
        },
74
        'timezone': {
75
            'type': 'string',
76
            'default': 'UTC'
77
        }
78
    },
79
    'credentials': {
80
        'username': {
81
            'type': 'string',
82
            'default': None
83
        },
84
        'password': {
85
            'type': 'string',
86
            'default': None
87
        },
88
        'api_key': {
89
            'type': 'string',
90
            'default': None
91
        }
92
    },
93
    'api': {
94
        'url': {
95
            'type': 'string',
96
            'default': None
97
        }
98
    },
99
    'auth': {
100
        'url': {
101
            'type': 'string',
102
            'default': None
103
        }
104
    },
105
    'stream': {
106
        'url': {
107
            'type': 'string',
108
            'default': None
109
        }
110
    }
111
}
112
113
CONFIG_DEFAULT_VALUES = {}
114
115
for section, keys in six.iteritems(CONFIG_FILE_OPTIONS):
116
    CONFIG_DEFAULT_VALUES[section] = {}
117
118
    for key, options in six.iteritems(keys):
119
        default_value = options['default']
120
        CONFIG_DEFAULT_VALUES[section][key] = default_value
121
122
123
class CLIConfigParser(object):
124
    def __init__(self, config_file_path, validate_config_exists=True,
125
                 validate_config_permissions=True, log=None):
126
        if validate_config_exists and not os.path.isfile(config_file_path):
127
            raise ValueError('Config file "%s" doesn\'t exist')
128
129
        if log is None:
130
            log = logging.getLogger(__name__)
131
            logging.basicConfig()
132
133
        self.config_file_path = config_file_path
134
        self.validate_config_permissions = validate_config_permissions
135
        self.LOG = log
136
137
    def parse(self):
138
        """
139
        Parse the config and return a dict with the parsed values.
140
141
        :rtype: ``dict``
142
        """
143
        result = defaultdict(dict)
144
145
        if not os.path.isfile(self.config_file_path):
146
            # Config doesn't exist, return the default values
147
            return CONFIG_DEFAULT_VALUES
148
149
        config_dir_path = os.path.dirname(self.config_file_path)
150
151
        if self.validate_config_permissions:
152
            # Make sure the directory permissions == 0o770
153
            if bool(os.stat(config_dir_path).st_mode & 0o777 ^ 0o770):
154
                self.LOG.warn(
155
                    # TODO: Perfect place for an f-string
156
                    "The StackStorm configuration directory permissions are "
157
                    "insecure (too permissive)."
158
                    "\n\n"
159
                    "You can fix this by running:"
160
                    "\n\n"
161
                    "    chmod 770 {config_dir}\n".format(config_dir=config_dir_path))
162
163
            # Make sure the setgid bit is set on the directory
164
            if not bool(os.stat(config_dir_path).st_mode & 0o2000):
165
                self.LOG.info(
166
                    # TODO: Perfect place for an f-string
167
                    "The SGID bit is not set on the StackStorm configuration "
168
                    "directory."
169
                    "\n\n"
170
                    "You can fix this by running:"
171
                    "\n\n"
172
                    "    chmod g+s {config_dir}\n".format(config_dir=config_dir_path))
173
174
            # Make sure the file permissions == 0o660
175
            if bool(os.stat(self.config_file_path).st_mode & 0o777 ^ 0o660):
176
                self.LOG.warn(
177
                    # TODO: Another perfect place for an f-string
178
                    "The StackStorm configuration file permissions are insecure."
179
                    "\n\n"
180
                    "You can fix this by running:"
181
                    "\n\n"
182
                    "    chmod 660 {config_file}\n".format(config_file=self.config_file_path))
183
184
        config = ConfigParser()
185
        with io.open(self.config_file_path, 'r', encoding='utf8') as fp:
186
            config.readfp(fp)
187
188
        for section, keys in six.iteritems(CONFIG_FILE_OPTIONS):
0 ignored issues
show
Comprehensibility Bug introduced by
section is re-defining a name which is already available in the outer-scope (previously defined on line 115).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
Comprehensibility Bug introduced by
keys is re-defining a name which is already available in the outer-scope (previously defined on line 115).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
189
            for key, options in six.iteritems(keys):
0 ignored issues
show
Comprehensibility Bug introduced by
key is re-defining a name which is already available in the outer-scope (previously defined on line 118).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
Comprehensibility Bug introduced by
options is re-defining a name which is already available in the outer-scope (previously defined on line 118).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
190
                key_type = options['type']
191
                key_default_value = options['default']
192
193
                if config.has_option(section, key):
194
                    if key_type in ['str', 'string']:
195
                        get_func = config.get
196
                    elif key_type in ['int', 'integer']:
197
                        get_func = config.getint
198
                    elif key_type in ['float']:
199
                        get_func = config.getfloat
200
                    elif key_type in ['bool', 'boolean']:
201
                        get_func = config.getboolean
202
                    else:
203
                        msg = 'Invalid type "%s" for option "%s"' % (key_type, key)
204
                        raise ValueError(msg)
205
206
                    value = get_func(section, key)
207
                    result[section][key] = value
208
                else:
209
                    result[section][key] = key_default_value
210
211
        return dict(result)
212