diff --git a/.verchew.ini b/.verchew.ini index 22975234..8b55ea76 100644 --- a/.verchew.ini +++ b/.verchew.ini @@ -17,6 +17,6 @@ version = 1 cli = dot cli_version_arg = -V -version = 7 +version = 7 || 8 optional = true message = This is only needed to generate UML diagrams for documentation. diff --git a/CHANGELOG.md b/CHANGELOG.md index c817edf1..56079726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.2 (beta) -- Added a `sync()` utility map arbitrary objects to the filesystem. +- Added a `sync()` utility to map arbitrary objects to the filesystem. ## 2.1.2 (2023-05-27) diff --git a/bin/verchew b/bin/verchew index 9ea5eebf..772ca4a6 100755 --- a/bin/verchew +++ b/bin/verchew @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - +# # The MIT License (MIT) # Copyright © 2016, Jace Browning # @@ -37,7 +37,6 @@ import sys from collections import OrderedDict from subprocess import PIPE, STDOUT, Popen - PY2 = sys.version_info[0] == 2 if PY2: @@ -47,13 +46,16 @@ else: import configparser from urllib.request import urlretrieve -__version__ = '3.1.1' +__version__ = "3.4.1" SCRIPT_URL = ( "https://raw.githubusercontent.com/jacebrowning/verchew/main/verchew/script.py" ) +WRAPPER_URL = ( + "https://raw.githubusercontent.com/jacebrowning/verchew/main/verchew/wrapper.sh" +) -CONFIG_FILENAMES = ['verchew.ini', '.verchew.ini', '.verchewrc', '.verchew'] +CONFIG_FILENAMES = ["verchew.ini", ".verchew.ini", ".verchewrc", ".verchew"] SAMPLE_CONFIG = """ [Python] @@ -108,11 +110,12 @@ def main(): if args.quiet: QUIET = True - log.debug("PWD: %s", os.getenv('PWD')) - log.debug("PATH: %s", os.getenv('PATH')) + log.debug("PWD: %s", os.getenv("PWD")) + log.debug("PATH: %s", os.getenv("PATH")) if args.vendor: - vendor_script(args.vendor) + vendor_script(SCRIPT_URL, args.vendor) + vendor_script(WRAPPER_URL, args.vendor + "-wrapper") sys.exit(0) path = find_config(args.root, generate=args.init) @@ -123,36 +126,34 @@ def main(): def parse_args(): - parser = argparse.ArgumentParser(description="System dependency version checker.",) + parser = argparse.ArgumentParser(description="System dependency version checker.") version = "%(prog)s v" + __version__ + parser.add_argument("--version", action="version", version=version) parser.add_argument( - '--version', action='version', version=version, - ) - parser.add_argument( - '-r', '--root', metavar='PATH', help="specify a custom project root directory" + "-r", "--root", metavar="PATH", help="specify a custom project root directory" ) parser.add_argument( - '--exit-code', - action='store_true', + "--exit-code", + action="store_true", help="return a non-zero exit code on failure", ) group_logging = parser.add_mutually_exclusive_group() group_logging.add_argument( - '-v', '--verbose', action='count', default=0, help="enable verbose logging" + "-v", "--verbose", action="count", default=0, help="enable verbose logging" ) group_logging.add_argument( - '-q', '--quiet', action='store_true', help="suppress all output on success" + "-q", "--quiet", action="store_true", help="suppress all output on success" ) - group_commands = parser.add_argument_group('commands') + group_commands = parser.add_argument_group("commands") group_commands.add_argument( - '--init', action='store_true', help="generate a sample configuration file" + "--init", action="store_true", help="generate a sample configuration file" ) group_commands.add_argument( - '--vendor', metavar='PATH', help="download the program for offline use" + "--vendor", metavar="PATH", help="download the program for offline use" ) args = parser.parse_args() @@ -171,14 +172,14 @@ def configure_logging(count=0): logging.basicConfig(level=level, format="%(levelname)s: %(message)s") -def vendor_script(path): +def vendor_script(url, path): root = os.path.abspath(os.path.join(path, os.pardir)) if not os.path.isdir(root): log.info("Creating directory %s", root) os.makedirs(root) - log.info("Downloading %s to %s", SCRIPT_URL, path) - urlretrieve(SCRIPT_URL, path) + log.info("Downloading %s to %s", url, path) + urlretrieve(url, path) log.debug("Making %s executable", path) mode = os.stat(path).st_mode @@ -213,8 +214,8 @@ def generate_config(root=None, filenames=None): path = os.path.join(root, filenames[0]) log.info("Generating sample config: %s", path) - with open(path, 'w') as config: - config.write(SAMPLE_CONFIG + '\n') + with open(path, "w") as config: + config.write(SAMPLE_CONFIG + "\n") return path @@ -232,9 +233,13 @@ def parse_config(path): data[section][name] = value for name in data: - version = data[name].get('version') or "" - data[name]['version'] = version - data[name]['patterns'] = [v.strip() for v in version.split('||')] + version = data[name].get("version") or "" + data[name]["version"] = version + data[name]["patterns"] = [v.strip() for v in version.split("||")] + + data[name]["optional"] = data[name].get( + "optional", "false" + ).strip().lower() in ("true", "yes", "y", True) return data @@ -244,32 +249,32 @@ def check_dependencies(config): for name, settings in config.items(): show("Checking for {0}...".format(name), head=True) - output = get_version(settings['cli'], settings.get('cli_version_arg')) + output = get_version(settings["cli"], settings.get("cli_version_arg")) - for pattern in settings['patterns']: + for pattern in settings["patterns"]: if match_version(pattern, output): show(_("~") + " MATCHED: {0}".format(pattern or "")) success.append(_("~")) break else: - if settings.get('optional'): - show(_("?") + " EXPECTED (OPTIONAL): {0}".format(settings['version'])) + if settings.get("optional"): + show(_("?") + " EXPECTED (OPTIONAL): {0}".format(settings["version"])) success.append(_("?")) else: if QUIET: if "not found" in output: actual = "Not found" else: - actual = output.split('\n')[0].strip('.') - expected = settings['version'] or "" + actual = output.split("\n", maxsplit=1)[0].strip(".") + expected = settings["version"] or "" print("{0}: {1}, EXPECTED: {2}".format(name, actual, expected)) show( _("x") - + " EXPECTED: {0}".format(settings['version'] or "") + + " EXPECTED: {0}".format(settings["version"] or "") ) success.append(_("x")) - if settings.get('message'): - show(_("#") + " MESSAGE: {0}".format(settings['message'])) + if settings.get("message"): + show(_("#") + " MESSAGE: {0}".format(settings["message"])) show("Results: " + " ".join(success), head=True) @@ -278,32 +283,46 @@ def check_dependencies(config): def get_version(program, argument=None): if argument is None: - args = [program, '--version'] + args = [program, "--version"] elif argument: - args = [program, argument] + args = [program] + argument.split() else: args = [program] show("$ {0}".format(" ".join(args))) output = call(args) lines = output.splitlines() - show(lines[0] if lines else "") + + if lines: + for line in lines: + if any(char.isdigit() for char in line): + show(line) + break + else: + show(lines[0]) + else: + show("") return output def match_version(pattern, output): - if "not found" in output.split('\n')[0]: + lines = output.splitlines() + if not lines or "not found" in lines[0]: return False - regex = pattern.replace('.', r'\.') + r'(\b|/)' + regex = pattern.replace(".", r"\.") + r"(\b|/)" - log.debug("Matching %s: %s", regex, output) - match = re.match(regex, output) - if match is None: - match = re.match(r'.*[^\d.]' + regex, output) + for line in lines: + log.debug("Matching %s: %s", regex, line) + match = re.match(regex, line) + if match is None: + log.debug("Matching %s: %s", regex, line) + match = re.match(r".*[^\d.]" + regex, line) + if match: + return True - return bool(match) + return False def call(args): @@ -314,27 +333,27 @@ def call(args): output = "sh: command not found: {0}".format(args[0]) else: raw = process.communicate()[0] - output = raw.decode('utf-8').strip() + output = raw.decode("utf-8").strip() log.debug("Command output: %r", output) return output -def show(text, start='', end='\n', head=False): +def show(text, start="", end="\n", head=False): """Python 2 and 3 compatible version of print.""" if QUIET: return if head: - start = '\n' - end = '\n\n' + start = "\n" + end = "\n\n" if log.getEffectiveLevel() < logging.WARNING: log.info(text) else: formatted = start + text + end if PY2: - formatted = formatted.encode('utf-8') + formatted = formatted.encode("utf-8") sys.stdout.write(formatted) sys.stdout.flush() @@ -344,11 +363,11 @@ def _(word, is_tty=None, supports_utf8=None, supports_ansi=None): formatted = word if is_tty is None: - is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() + is_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() if supports_utf8 is None: - supports_utf8 = str(sys.stdout.encoding).lower() == 'utf-8' + supports_utf8 = str(sys.stdout.encoding).lower() == "utf-8" if supports_ansi is None: - supports_ansi = sys.platform != 'win32' or 'ANSICON' in os.environ + supports_ansi = sys.platform != "win32" or "ANSICON" in os.environ style_support = supports_utf8 color_support = is_tty and supports_ansi @@ -362,5 +381,5 @@ def _(word, is_tty=None, supports_utf8=None, supports_ansi=None): return formatted -if __name__ == '__main__': # pragma: no cover +if __name__ == "__main__": # pragma: no cover main()