Completed
Push — develop ( 520f16...c27fb6 )
by Jace
15s queued 11s
created

verchew.script   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Test Coverage

Coverage 95.71%

Importance

Changes 0
Metric Value
eloc 201
dl 0
loc 340
ccs 134
cts 140
cp 0.9571
rs 8.48
c 0
b 0
f 0
wmc 49

12 Functions

Rating   Name   Duplication   Size   Complexity  
A match_version() 0 9 2
A parse_args() 0 21 1
A main() 0 16 4
A show() 0 17 5
A call() 0 12 3
A configure_logging() 0 9 3
B _() 0 21 7
B check_dependencies() 0 30 8
A find_config() 0 19 4
A get_version() 0 13 4
B parse_config() 0 33 6
A generate_config() 0 11 2

How to fix   Complexity   

Complexity

Complex classes like verchew.script 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
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
4
# The MIT License (MIT)
5
# Copyright © 2016, Jace Browning
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining a copy
8
# of this software and associated documentation files (the "Software"), to deal
9
# in the Software without restriction, including without limitation the rights
10
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
# copies of the Software, and to permit persons to whom the Software is
12
# furnished to do so, subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included in
15
# all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
# SOFTWARE.
24
#
25
# Source: https://github.com/jacebrowning/verchew
26
# Documentation: https://verchew.readthedocs.io
27
# Package: https://pypi.org/project/verchew
28
29
30 1
from __future__ import unicode_literals
31
32 1
import argparse
33 1
import logging
34 1
import os
35 1
import re
36 1
import sys
37
import warnings
38
from collections import OrderedDict
39 1
from subprocess import PIPE, STDOUT, Popen
40 1
41 1
42
try:
43 1
    import configparser  # Python 3
44
except ImportError:
45 1
    import ConfigParser as configparser  # Python 2
46 1
47 1
__version__ = '1.6.2'
48
49
PY2 = sys.version_info[0] == 2
50
51
CONFIG_FILENAMES = [
52
    'verchew.ini',
53
    '.verchew.ini',
54
    '.verchewrc',
55
    '.verchew',
56
]
57
58
SAMPLE_CONFIG = """
59
[Python]
60
61
cli = python
62
version = Python 3.5 || Python 3.6
63
64
[Legacy Python]
65
66
cli = python2
67
version = Python 2.7
68
69
[virtualenv]
70
71
cli = virtualenv
72
version = 15
73 1
message = Only required with Python 2.
74
75
[Make]
76
77
cli = make
78
version = GNU Make
79 1
optional = true
80
81
""".strip()
82
83
STYLE = {
84
    "~": "✔",
85
    "*": "⭑",
86
    "?": "⚠",
87 1
    "x": "✘",
88
}
89
90 1
COLOR = {
91 1
    "x": "\033[91m",  # red
92 1
    "~": "\033[92m",  # green
93
    "?": "\033[93m",  # yellow
94 1
    "*": "\033[94m",  # cyan
95 1
    None: "\033[0m",  # reset
96
}
97 1
98 1
QUIET = False
99
100 1
log = logging.getLogger(__name__)
101 1
102
103
def main():
104 1
    global QUIET
105 1
106
    args = parse_args()
107 1
    configure_logging(args.verbose)
108 1
    if args.quiet:
109 1
        QUIET = True
110
111 1
    log.debug("PWD: %s", os.getenv('PWD'))
112
    log.debug("PATH: %s", os.getenv('PATH'))
113 1
114
    path = find_config(args.root, generate=args.init)
115 1
    config = parse_config(path)
116
117
    if not check_dependencies(config) and args.exit_code:
118 1
        sys.exit(1)
119
120 1
121
def parse_args():
122
    parser = argparse.ArgumentParser()
123 1
124 1
    version = "%(prog)s v" + __version__
125 1
    parser.add_argument('--version', action='version', version=version)
126
    parser.add_argument('-r', '--root', metavar='PATH',
127
                        help="specify a custom project root directory")
128
    parser.add_argument('--init', action='store_true',
129
                        help="generate a sample configuration file")
130
    parser.add_argument('--exit-code', action='store_true',
131 1
                        help="return a non-zero exit code on failure")
132
133
    group = parser.add_mutually_exclusive_group()
134 1
    group.add_argument('-v', '--verbose', action='count', default=0,
135 1
                       help="enable verbose logging")
136 1
    group.add_argument('-q', '--quiet', action='store_true',
137
                       help="suppress all output on success")
138 1
139 1
    args = parser.parse_args()
140 1
141 1
    return args
142 1
143 1
144 1
def configure_logging(count=0):
145 1
    if count == 0:
146
        level = logging.WARNING
147 1
    elif count == 1:
148 1
        level = logging.INFO
149 1
    else:
150
        level = logging.DEBUG
151 1
152 1
    logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
153
154
155 1
def find_config(root=None, filenames=None, generate=False):
156 1
    root = root or os.getcwd()
157 1
    filenames = filenames or CONFIG_FILENAMES
158
159 1
    path = None
160
    log.info("Looking for config file in: %s", root)
161 1
    log.debug("Filename options: %s", ", ".join(filenames))
162 1
    for filename in os.listdir(root):
163 1
        if filename in filenames:
164
            path = os.path.join(root, filename)
165 1
            log.info("Found config file: %s", path)
166
            return path
167
168 1
    if generate:
169 1
        path = generate_config(root, filenames)
170
        return path
171 1
172 1
    msg = "No config file found in: {0}".format(root)
173 1
    raise RuntimeError(msg)
174
175 1
176 1
def generate_config(root=None, filenames=None):
177 1
    root = root or os.getcwd()
178 1
    filenames = filenames or CONFIG_FILENAMES
179
180 1
    path = os.path.join(root, filenames[0])
181
182
    log.info("Generating sample config: %s", path)
183 1
    with open(path, 'w') as config:
184 1
        config.write(SAMPLE_CONFIG + '\n')
185
186 1
    return path
187 1
188 1
189 1
def parse_config(path):
190 1
    data = OrderedDict()
191 1
192
    log.info("Parsing config file: %s", path)
193 1
    config = configparser.ConfigParser()
194 1
    config.read(path)
195 1
196
    for section in config.sections():
197 1
        data[section] = OrderedDict()
198 1
        for name, value in config.items(section):
199 1
            data[section][name] = value
200 1
201
    for name in data:
202 1
        if 'versions' in data[name]:
203
            warnings.warn(
204 1
                "'versions' is deprecated, use 'version' instead",
205
                DeprecationWarning
206
            )
207 1
            version = data[name].pop('versions') or ""
208 1
        else:
209 1
            version = data[name].get('version') or ""
210
211 1
        if ' | ' in version:
212 1
            warnings.warn(
213 1
                "'|' is deprecated, use '||' to separate multiple versions",
214
                DeprecationWarning
215 1
            )
216
            version = version.replace(' | ', ' || ')
217
218 1
        data[name]['version'] = version
219 1
        data[name]['patterns'] = [v.strip() for v in version.split('||')]
220
221
    return data
222 1
223 1
224 1
def check_dependencies(config):
225 1
    success = []
226 1
227 1
    for name, settings in config.items():
228
        show("Checking for {0}...".format(name), head=True)
229 1
        output = get_version(settings['cli'], settings.get('cli_version_arg'))
230 1
231 1
        for pattern in settings['patterns']:
232
            if match_version(pattern, output):
233 1
                show(_("~") + " MATCHED: {0}".format(pattern))
234
                success.append(_("~"))
235
                break
236 1
        else:
237
            if settings.get('optional'):
238 1
                show(_("?") + " EXPECTED: {0}".format(settings['version']))
239 1
                success.append(_("?"))
240 1
            else:
241
                if QUIET:
242 1
                    print("Unmatched {0} version: {1}".format(
243 1
                        name,
244
                        settings['version'],
245 1
                    ))
246 1
                show(_("x") + " EXPECTED: {0}".format(settings['version']))
247
                success.append(_("x"))
248 1
            if settings.get('message'):
249 1
                show(_("*") + " MESSAGE: {0}".format(settings['message']))
250
251
    show("Results: " + " ".join(success), head=True)
252 1
253
    return _("x") not in success
254 1
255
256 1
def get_version(program, argument=None):
257 1
    if argument is None:
258 1
        args = [program, '--version']
259 1
    elif argument:
260 1
        args = [program, argument]
261 1
    else:
262
        args = [program]
263 1
264 1
    show("$ {0}".format(" ".join(args)))
265
    output = call(args)
266 1
    show(output.splitlines()[0] if output else "<nothing>")
267 1
268
    return output
269 1
270 1
271
def match_version(pattern, output):
272 1
    regex = pattern.replace('.', r'\.') + r'(\b|/)'
273
274
    log.debug("Matching %s: %s", regex, output)
275
    match = re.match(regex, output)
276
    if match is None:
277
        match = re.match(r'.*[^\d.]' + regex, output)
278
279
    return bool(match)
280
281
282
def call(args):
283
    try:
284
        process = Popen(args, stdout=PIPE, stderr=STDOUT)
285
    except OSError:
286
        log.debug("Command not found: %s", args[0])
287
        output = "sh: command not found: {0}".format(args[0])
288
    else:
289
        raw = process.communicate()[0]
290
        output = raw.decode('utf-8').strip()
291
        log.debug("Command output: %r", output)
292
293
    return output
294
295
296
def show(text, start='', end='\n', head=False):
297
    """Python 2 and 3 compatible version of print."""
298
    if QUIET:
299
        return
300
301
    if head:
302
        start = '\n'
303
        end = '\n\n'
304
305
    if log.getEffectiveLevel() < logging.WARNING:
306
        log.info(text)
307
    else:
308
        formatted = (start + text + end)
309
        if PY2:
310
            formatted = formatted.encode('utf-8')
311
        sys.stdout.write(formatted)
312
        sys.stdout.flush()
313
314
315
def _(word, is_tty=None, supports_utf8=None, supports_ansi=None):
316
    """Format and colorize a word based on available encoding."""
317
    formatted = word
318
319
    if is_tty is None:
320
        is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
321
    if supports_utf8 is None:
322
        supports_utf8 = sys.stdout.encoding == 'UTF-8'
323
    if supports_ansi is None:
324
        supports_ansi = sys.platform != 'win32' or 'ANSICON' in os.environ
325
326
    style_support = supports_utf8
327
    color_support = is_tty and supports_ansi
328
329
    if style_support:
330
        formatted = STYLE.get(word, word)
331
332
    if color_support and COLOR.get(word):
333
        formatted = COLOR[word] + formatted + COLOR[None]
334
335
    return formatted
336
337
338
if __name__ == '__main__':  # pragma: no cover
339
    main()
340