Passed
Push — develop ( 6195a6...399285 )
by Jace
01:47
created

verchew.script._()   B

Complexity

Conditions 7

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 15
nop 4
dl 0
loc 21
rs 8
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 7
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 sys
36 1
from collections import OrderedDict
37
from subprocess import PIPE, STDOUT, Popen
38
39 1
40 1
try:
41 1
    import configparser  # Python 3
42
except ImportError:
43 1
    import ConfigParser as configparser  # Python 2
44
45 1
__version__ = '1.4'
46 1
47 1
PY2 = sys.version_info[0] == 2
48
CONFIG_FILENAMES = [
49
    'verchew.ini',
50
    '.verchew.ini',
51
    '.verchewrc',
52
    '.verchew',
53
]
54
SAMPLE_CONFIG = """
55
[Python]
56
57
cli = python
58
versions = Python 3.5 | Python 3.6
59
60
[Legacy Python]
61
62
cli = python2
63
version = Python 2.7
64
65
[virtualenv]
66
67
cli = virtualenv
68
version = 15.
69
message = Only required with Python 2.
70
71
[Make]
72
73 1
cli = make
74
version = GNU Make
75
optional = true
76
77
""".strip()
78
STYLE = {
79 1
    "~": "✔",
80
    "*": "⭑",
81
    "?": "⚠",
82
    "x": "✘",
83
}
84
COLOR = {
85
    "x": "\033[91m",  # red
86
    "~": "\033[92m",  # green
87 1
    "?": "\033[93m",  # yellow
88
    "*": "\033[94m",  # cyan
89
    None: "\033[0m",  # reset
90 1
}
91 1
92 1
log = logging.getLogger(__name__)
93
94 1
95 1
def main():
96
    args = parse_args()
97 1
    configure_logging(args.verbose)
98 1
99
    log.debug("PWD: %s", os.getenv('PWD'))
100 1
    log.debug("PATH: %s", os.getenv('PATH'))
101 1
102
    path = find_config(args.root, generate=args.init)
103
    config = parse_config(path)
104 1
105 1
    if not check_dependencies(config) and args.exit_code:
106
        sys.exit(1)
107 1
108 1
109 1
def parse_args():
110
    parser = argparse.ArgumentParser()
111 1
112
    version = "%(prog)s v" + __version__
113 1
    parser.add_argument('--version', action='version', version=version)
114
    parser.add_argument('-r', '--root', metavar='PATH',
115 1
                        help="specify a custom project root directory")
116
    parser.add_argument('--init', action='store_true',
117
                        help="generate a sample configuration file")
118 1
    parser.add_argument('--exit-code', action='store_true',
119
                        help="return a non-zero exit code on failure")
120 1
    parser.add_argument('-v', '--verbose', action='count', default=0,
121
                        help="enable verbose logging")
122
123 1
    args = parser.parse_args()
124 1
125 1
    return args
126
127
128
def configure_logging(count=0):
129
    if count == 0:
130
        level = logging.WARNING
131 1
    elif count == 1:
132
        level = logging.INFO
133
    else:
134 1
        level = logging.DEBUG
135 1
136 1
    logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
137
138 1
139 1
def find_config(root=None, filenames=None, generate=False):
140 1
    root = root or os.getcwd()
141 1
    filenames = filenames or CONFIG_FILENAMES
142 1
143 1
    path = None
144 1
    log.info("Looking for config file in: %s", root)
145 1
    log.debug("Filename options: %s", ", ".join(filenames))
146
    for filename in os.listdir(root):
147 1
        if filename in filenames:
148 1
            path = os.path.join(root, filename)
149 1
            log.info("Found config file: %s", path)
150
            return path
151 1
152 1
    if generate:
153
        path = generate_config(root, filenames)
154
        return path
155 1
156 1
    msg = "No config file found in: {0}".format(root)
157 1
    raise RuntimeError(msg)
158
159 1
160
def generate_config(root=None, filenames=None):
161 1
    root = root or os.getcwd()
162 1
    filenames = filenames or CONFIG_FILENAMES
163 1
164
    path = os.path.join(root, filenames[0])
165 1
166
    log.info("Generating sample config: %s", path)
167
    with open(path, 'w') as config:
168 1
        config.write(SAMPLE_CONFIG + '\n')
169 1
170
    return path
171 1
172 1
173 1
def parse_config(path):
174
    data = OrderedDict()
175 1
176 1
    log.info("Parsing config file: %s", path)
177 1
    config = configparser.ConfigParser()
178 1
    config.read(path)
179
180 1
    for section in config.sections():
181
        data[section] = OrderedDict()
182
        for name, value in config.items(section):
183 1
            data[section][name] = value
184 1
185
    for name in data:
186 1
        versions = data[name].get('versions', data[name].pop('version', ""))
187 1
        data[name]['versions'] = versions
188 1
        data[name]['patterns'] = [v.strip() for v in versions.split('|')]
189 1
190 1
    return data
191 1
192
193 1
def check_dependencies(config):
194 1
    success = []
195 1
196
    for name, settings in config.items():
197 1
        show("Checking for {0}...".format(name), head=True)
198 1
        output = get_version(settings['cli'], settings.get('cli_version_arg'))
199 1
200 1
        for pattern in settings['patterns']:
201
            if match_version(pattern, output):
202 1
                show(_("~") + " MATCHED: {0}".format(pattern))
203
                success.append(_("~"))
204 1
                break
205
        else:
206
            if settings.get('optional'):
207 1
                show(_("?") + " EXPECTED: {0}".format(settings['versions']))
208 1
                success.append(_("?"))
209 1
            else:
210
                show(_("x") + " EXPECTED: {0}".format(settings['versions']))
211 1
                success.append(_("x"))
212 1
            if settings.get('message'):
213 1
                show(_("*") + " MESSAGE: {0}".format(settings['message']))
214
215 1
    show("Results: " + " ".join(success), head=True)
216
217
    return _("x") not in success
218 1
219 1
220
def get_version(program, argument=None):
221
    if argument is None:
222 1
        args = [program, '--version']
223 1
    elif argument:
224 1
        args = [program, argument]
225 1
    else:
226 1
        args = [program]
227 1
228
    show("$ {0}".format(" ".join(args)))
229 1
    output = call(args)
230 1
    show(output.splitlines()[0])
231 1
232
    return output
233 1
234
235
def match_version(pattern, output):
236 1
    return output.startswith(pattern) or " " + pattern in output
237
238 1
239 1
def call(args):
240 1
    try:
241
        process = Popen(args, stdout=PIPE, stderr=STDOUT)
242 1
    except OSError:
243 1
        log.debug("Command not found: %s", args[0])
244
        output = "sh: command not found: {0}".format(args[0])
245 1
    else:
246 1
        raw = process.communicate()[0]
247
        output = raw.decode('utf-8').strip()
248 1
        log.debug("Command output: %r", output)
249 1
250
    return output
251
252 1
253
def show(text, start='', end='\n', head=False):
254 1
    """Python 2 and 3 compatible version of print."""
255
    if head:
256 1
        start = '\n'
257 1
        end = '\n\n'
258 1
259 1
    if log.getEffectiveLevel() < logging.WARNING:
260 1
        log.info(text)
261 1
    else:
262
        formatted = (start + text + end)
263 1
        if PY2:
264 1
            formatted = formatted.encode('utf-8')
265
        sys.stdout.write(formatted)
266 1
        sys.stdout.flush()
267 1
268
269 1
def _(word, is_tty=None, supports_utf8=None, supports_ansi=None):
270 1
    """Format and colorize a word based on available encoding."""
271
    formatted = word
272 1
273
    if is_tty is None:
274
        is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
275
    if supports_utf8 is None:
276
        supports_utf8 = sys.stdout.encoding == 'UTF-8'
277
    if supports_ansi is None:
278
        supports_ansi = sys.platform != 'win32' or 'ANSICON' in os.environ
279
280
    style_support = supports_utf8
281
    color_support = is_tty and supports_ansi
282
283
    if style_support:
284
        formatted = STYLE.get(word, word)
285
286
    if color_support and COLOR.get(word):
287
        formatted = COLOR[word] + formatted + COLOR[None]
288
289
    return formatted
290
291
292
if __name__ == '__main__':  # pragma: no cover
293
    main()
294