Completed
Push — develop ( 7113ee...32aa45 )
by Jace
13s queued 11s
created

verchew.script.vendor_script()   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 12
rs 9.9
c 0
b 0
f 0
ccs 9
cts 9
cp 1
cc 2
nop 1
crap 2
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
from collections import OrderedDict
38
from subprocess import PIPE, STDOUT, Popen
39 1
40 1
41 1
PY2 = sys.version_info[0] == 2
42
43 1
if PY2:
44
    import ConfigParser as configparser
45 1
    from urllib import urlretrieve
46 1
else:
47 1
    import configparser
48
    from urllib.request import urlretrieve
49
50
__version__ = '3.1b1'
51
52
SCRIPT_URL = (
53
    "https://raw.githubusercontent.com/jacebrowning/verchew/master/verchew/script.py"
54
)
55
56
CONFIG_FILENAMES = ['verchew.ini', '.verchew.ini', '.verchewrc', '.verchew']
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 = {"~": "✔", "*": "⭑", "?": "⚠", "x": "✘"}
84
85
COLOR = {
86
    "x": "\033[91m",  # red
87 1
    "~": "\033[92m",  # green
88
    "?": "\033[93m",  # yellow
89
    "*": "\033[94m",  # cyan
90 1
    None: "\033[0m",  # reset
91 1
}
92 1
93
QUIET = False
94 1
95 1
log = logging.getLogger(__name__)
96
97 1
98 1
def main():
99
    global QUIET
100 1
101 1
    args = parse_args()
102
    configure_logging(args.verbose)
103
    if args.quiet:
104 1
        QUIET = True
105 1
106
    log.debug("PWD: %s", os.getenv('PWD'))
107 1
    log.debug("PATH: %s", os.getenv('PATH'))
108 1
109 1
    if args.vendor:
110
        vendor_script(args.vendor)
111 1
        sys.exit(0)
112
113 1
    path = find_config(args.root, generate=args.init)
114
    config = parse_config(path)
115 1
116
    if not check_dependencies(config) and args.exit_code:
117
        sys.exit(1)
118 1
119
120 1
def parse_args():
121
    parser = argparse.ArgumentParser(description="System dependency version checker.",)
122
123 1
    version = "%(prog)s v" + __version__
124 1
    parser.add_argument(
125 1
        '--version', action='version', version=version,
126
    )
127
    parser.add_argument(
128
        '-r', '--root', metavar='PATH', help="specify a custom project root directory"
129
    )
130
    parser.add_argument(
131 1
        '--exit-code',
132
        action='store_true',
133
        help="return a non-zero exit code on failure",
134 1
    )
135 1
136 1
    group_logging = parser.add_mutually_exclusive_group()
137
    group_logging.add_argument(
138 1
        '-v', '--verbose', action='count', default=0, help="enable verbose logging"
139 1
    )
140 1
    group_logging.add_argument(
141 1
        '-q', '--quiet', action='store_true', help="suppress all output on success"
142 1
    )
143 1
144 1
    group_commands = parser.add_argument_group('commands')
145 1
    group_commands.add_argument(
146
        '--init', action='store_true', help="generate a sample configuration file"
147 1
    )
148 1
149 1
    group_commands.add_argument(
150
        '--vendor', metavar='PATH', help="download the program for offline use"
151 1
    )
152 1
153
    args = parser.parse_args()
154
155 1
    return args
156 1
157 1
158
def configure_logging(count=0):
159 1
    if count == 0:
160
        level = logging.WARNING
161 1
    elif count == 1:
162 1
        level = logging.INFO
163 1
    else:
164
        level = logging.DEBUG
165 1
166
    logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
167
168 1
169 1
def vendor_script(path):
170
    root = os.path.abspath(os.path.join(path, os.pardir))
171 1
    if not os.path.isdir(root):
172 1
        log.info("Creating directory %s", root)
173 1
        os.makedirs(root)
174
175 1
    log.info("Downloading %s to %s", SCRIPT_URL, path)
176 1
    urlretrieve(SCRIPT_URL, path)
177 1
178 1
    log.debug("Making %s executable", path)
179
    mode = os.stat(path).st_mode
180 1
    os.chmod(path, mode | 0o111)
181
182
183 1
def find_config(root=None, filenames=None, generate=False):
184 1
    root = root or os.getcwd()
185
    filenames = filenames or CONFIG_FILENAMES
186 1
187 1
    path = None
188 1
    log.info("Looking for config file in: %s", root)
189 1
    log.debug("Filename options: %s", ", ".join(filenames))
190 1
    for filename in os.listdir(root):
191 1
        if filename in filenames:
192
            path = os.path.join(root, filename)
193 1
            log.info("Found config file: %s", path)
194 1
            return path
195 1
196
    if generate:
197 1
        path = generate_config(root, filenames)
198 1
        return path
199 1
200 1
    msg = "No config file found in: {0}".format(root)
201
    raise RuntimeError(msg)
202 1
203
204 1
def generate_config(root=None, filenames=None):
205
    root = root or os.getcwd()
206
    filenames = filenames or CONFIG_FILENAMES
207 1
208 1
    path = os.path.join(root, filenames[0])
209 1
210
    log.info("Generating sample config: %s", path)
211 1
    with open(path, 'w') as config:
212 1
        config.write(SAMPLE_CONFIG + '\n')
213 1
214
    return path
215 1
216
217
def parse_config(path):
218 1
    data = OrderedDict()  # type: ignore
219 1
220
    log.info("Parsing config file: %s", path)
221
    config = configparser.ConfigParser()
222 1
    config.read(path)
223 1
224 1
    for section in config.sections():
225 1
        data[section] = OrderedDict()
226 1
        for name, value in config.items(section):
227 1
            data[section][name] = value
228
229 1
    for name in data:
230 1
        version = data[name].get('version') or ""
231 1
        data[name]['version'] = version
232
        data[name]['patterns'] = [v.strip() for v in version.split('||')]
233 1
234
    return data
235
236 1
237
def check_dependencies(config):
238 1
    success = []
239 1
240 1
    for name, settings in config.items():
241
        show("Checking for {0}...".format(name), head=True)
242 1
        output = get_version(settings['cli'], settings.get('cli_version_arg'))
243 1
244
        for pattern in settings['patterns']:
245 1
            if match_version(pattern, output):
246 1
                show(_("~") + " MATCHED: {0}".format(pattern or "<anything>"))
247
                success.append(_("~"))
248 1
                break
249 1
        else:
250
            if settings.get('optional'):
251
                show(_("?") + " EXPECTED: {0}".format(settings['version']))
252 1
                success.append(_("?"))
253
            else:
254 1
                if QUIET:
255
                    if "not found" in output:
256 1
                        actual = "Not found"
257 1
                    else:
258 1
                        actual = output.split('\n')[0].strip('.')
259 1
                    expected = settings['version'] or "<anything>"
260 1
                    print("{0}: {1}, EXPECTED: {2}".format(name, actual, expected))
261 1
                show(
262
                    _("x")
263 1
                    + " EXPECTED: {0}".format(settings['version'] or "<anything>")
264 1
                )
265
                success.append(_("x"))
266 1
            if settings.get('message'):
267 1
                show(_("*") + " MESSAGE: {0}".format(settings['message']))
268
269 1
    show("Results: " + " ".join(success), head=True)
270 1
271
    return _("x") not in success
272 1
273
274
def get_version(program, argument=None):
275
    if argument is None:
276
        args = [program, '--version']
277
    elif argument:
278
        args = [program, argument]
279
    else:
280
        args = [program]
281
282
    show("$ {0}".format(" ".join(args)))
283
    output = call(args)
284
    lines = output.splitlines()
285
    show(lines[0] if lines else "<nothing>")
286
287
    return output
288
289
290
def match_version(pattern, output):
291
    if "not found" in output.split('\n')[0]:
292
        return False
293
294
    regex = pattern.replace('.', r'\.') + r'(\b|/)'
295
296
    log.debug("Matching %s: %s", regex, output)
297
    match = re.match(regex, output)
298
    if match is None:
299
        match = re.match(r'.*[^\d.]' + regex, output)
300
301
    return bool(match)
302
303
304
def call(args):
305
    try:
306
        process = Popen(args, stdout=PIPE, stderr=STDOUT)
307
    except OSError:
308
        log.debug("Command not found: %s", args[0])
309
        output = "sh: command not found: {0}".format(args[0])
310
    else:
311
        raw = process.communicate()[0]
312
        output = raw.decode('utf-8').strip()
313
        log.debug("Command output: %r", output)
314
315
    return output
316
317
318
def show(text, start='', end='\n', head=False):
319
    """Python 2 and 3 compatible version of print."""
320
    if QUIET:
321
        return
322
323
    if head:
324
        start = '\n'
325
        end = '\n\n'
326
327
    if log.getEffectiveLevel() < logging.WARNING:
328
        log.info(text)
329
    else:
330
        formatted = start + text + end
331
        if PY2:
332
            formatted = formatted.encode('utf-8')
333
        sys.stdout.write(formatted)
334
        sys.stdout.flush()
335
336
337
def _(word, is_tty=None, supports_utf8=None, supports_ansi=None):
338
    """Format and colorize a word based on available encoding."""
339
    formatted = word
340
341
    if is_tty is None:
342
        is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
343
    if supports_utf8 is None:
344
        supports_utf8 = str(sys.stdout.encoding).lower() == 'utf-8'
345
    if supports_ansi is None:
346
        supports_ansi = sys.platform != 'win32' or 'ANSICON' in os.environ
347
348
    style_support = supports_utf8
349
    color_support = is_tty and supports_ansi
350
351
    if style_support:
352
        formatted = STYLE.get(word, word)
353
354
    if color_support and COLOR.get(word):
355
        formatted = COLOR[word] + formatted + COLOR[None]
356
357
    return formatted
358
359
360
if __name__ == '__main__':  # pragma: no cover
361
    main()
362