Index: pluginconf/__init__.py ================================================================== --- pluginconf/__init__.py +++ pluginconf/__init__.py @@ -1,8 +1,8 @@ # encoding: utf-8 # api: python -# type: extract +##type: extract # category: config # title: Plugin configuration # description: Read meta data, pyz/package contents, module locating # version: 0.7.7 # state: stable @@ -57,16 +57,10 @@ # argparse_map() # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Converts a list of config: options with arg: attribute for use as # argparser parameters. # -# dependency().depends()/.valid() -# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Probes a new plugins` depends: list against installed base modules. -# Utilizes each version: fields and allows for virtual modules, or -# alternatives and honors alias: names. -# # # Generally this scheme concerns itself more with plugin basenames. # That is: module scripts in a package like `ext.plg1` and `ext.plg2`. # It can be initialized by injecting the plugin-package basename into # plugin_base = []. The associated paths will be used for module @@ -82,77 +76,101 @@ import sys import os import re +import functools import pkgutil import inspect try: - from distutils.spawn import find_executable -except: - try: - from compat2and3 import find_executable - except: - def find_executable(str): - pass -try: from gzip import decompress as gzip_decode # Py3 only -except: +except ImportError: try: from compat2and3 import gzip_decode # st2 stub - except: - def gzip_decode(bytes): - import zlib - return zlib.decompress(bytes, 16 + zlib.MAX_WBITS) # not fully compatible + except ImportError: + import zlib + def gzip_decode(bytestr): + """ haphazard workaround """ + return zlib.decompress(bytestr, 16 + zlib.MAX_WBITS) import zipfile import argparse __all__ = [ - "get_data", "module_list", "plugin_meta", - "dependency", "add_plugin_defaults" + "get_data", "module_list", "plugin_meta", "add_plugin_defaults" ] # Injectables # ‾‾‾‾‾‾‾‾‾‾‾ +""" injectable callback function for logging """ log_ERR = lambda *x: None -# File lookup relation for get_data(), should name a top-level package. -module_base = "config" # equivalent PluginBase(package=…) +""" +File lookup relation for get_data(), should name a top-level package. +(Equivalent PluginBase(package=…)) +""" +module_base = "config" + +""" +Package/module names for module_list() and plugin_meta() lookups. +All associated paths will be scanned for module/plugin basenames. +(Equivalent to `searchpath` in PluginBase) +""" +plugin_base = ["channels"] + -# Package/module names for module_list() and plugin_meta() lookups. -# All associated paths will be scanned for module/plugin basenames. -plugin_base = ["channels"] # equivalent to `searchpath` in PluginBase +# Compatiblity +# ‾‾‾‾‾‾‾‾‾‾‾‾ +def renamed_arguments(renamed): + """ map old argument names """ + def wrapped(func): + def execute(*args, **kwargs): + return func(*args, **{ + renamed.get(key, key): value + for key, value in kwargs.items() + }) + functools.update_wrapper(execute, func) + return execute + return wrapped # Resource retrieval # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Fetches file content from install path or from within PYZ -# archive. This is just an alias and convenience wrapper for -# pkgutil.get_data(). -# Utilizes the module_base / file_base as top-level reference. -# -def get_data(fn, decode=False, gz=False, file_base=None): +@renamed_arguments({"fn": "filename", "gz": "gzip"}) +def get_data(filename, decode=False, gzip=False, file_base=None): + """ + Fetches file content from install path or from within PYZ + archive. This is just an alias and convenience wrapper for + pkgutil.get_data(). + Utilizes the module_base / plugin_base as top-level reference. + + :arg str fn: filename in pyz or bundle + :arg bool decode: text file decoding utf-8 + :arg bool gz: automatic gzdecode + :arg str file_base: alternative base module reference + """ try: - bin = pkgutil.get_data(file_base or module_base, fn) - if gz: - bin = gzip_decode(bin) + data = pkgutil.get_data(file_base or module_base, filename) + if gzip: + data = gzip_decode(data) if decode: - return bin.decode("utf-8", errors='ignore') - else: - return str(bin) + return data.decode("utf-8", errors='ignore') + return str(data) except: # log_ERR("get_data() didn't find:", fn, "in", file_base) pass # Plugin name lookup # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Search through ./plugins/ (and other configured plugin_base -# names → paths) and get module basenames. -# -def module_list(extra_paths=[]): +def module_list(extra_paths=None): + """ + Search through ./plugins/ (and other configured plugin_base + names → paths) and get module basenames. + + :arg list extra_paths: in addition to plugin_base list + """ # Convert plugin_base package names into paths for iter_modules paths = [] for mp in plugin_base: if sys.modules.get(mp): @@ -159,61 +177,61 @@ paths += sys.modules[mp].__path__ elif os.path.exists(mp): paths.append(mp) # Should list plugins within zips as well as local paths - ls = pkgutil.iter_modules(paths + extra_paths) + ls = pkgutil.iter_modules(paths + (extra_paths or [])) return [name for loader, name, ispkg in ls] # Plugin => meta dict # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# This is a trivial wrapper to assemble a complete dictionary -# of available/installed plugins. It associates each plugin name -# with a its meta{} fields. -# def all_plugin_meta(): + """ + This is a trivial wrapper to assemble a complete dictionary + of available/installed plugins. It associates each plugin name + with a its meta{} fields. + """ return { name: plugin_meta(module=name) for name in module_list() } # Plugin meta data extraction # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Can fetch infos from different sources: -# -# fn= read literal files, or .pyz contents -# -# src= from already uncovered script code -# -# module= lookup per pkgutil, from plugin bases -# or top-level modules -# -# frame= extract comment header of caller -# (default) -# -def plugin_meta(fn=None, src=None, module=None, frame=1, extra_base=[], max_length=6144): +@renamed_arguments({"filename": "fn"}) +def plugin_meta(fn=None, src=None, module=None, frame=1, **kwargs): + """ + Extract plugin meta data block from different sources: + + :arg str fn: read literal files, or .pyz contents + :arg str src: from already uncovered script code + :arg str module: lookup per pkgutil, from plugin_base or top-level modules + :arg int frame: extract comment header of caller (default) + :arg list extra_base: additional search directories + :arg ist max_length: maximum size to read from files + """ # Try via pkgutil first, # find any plugins.* modules, or main packages if module: fn = module - for base in plugin_base + extra_base: + for base in plugin_base + kwargs.get("extra_base", []): try: src = get_data(fn=fn+".py", decode=True, file_base=base) if src: break except: continue # plugin_meta_extract() will print a notice later # Real filename/path elif fn and os.path.exists(fn): - src = open(fn).read(max_length) + src = open(fn).read(kwargs.get("max_length", 6144)) # Else get source directly from caller elif not src and not fn: - module = inspect.getmodule(sys._getframe(frame)) + module = inspect.getmodule(sys._getframe(frame+1)) # decorator+1 fn = inspect.getsourcefile(module) src = inspect.getcomments(module) # Assume it's a filename within a zip elif fn: @@ -232,15 +250,20 @@ return plugin_meta_extract(src, fn) # Comment and field extraction logic # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Finds the first comment block. Splits key:value header -# fields from comment. Turns everything into an dict, with -# some stub fields if absent. -# def plugin_meta_extract(src="", fn=None, literal=False): + """ + Finds the first comment block. Splits key:value header + fields from comment. Turns everything into an dict, with + some stub fields if absent. + + :arg str src: from existing source code + :arg int fn: set filename attribute + :arg bool literla: just split comment from doc + """ # Defaults meta = { "id": os.path.splitext(os.path.basename(fn or ""))[0], "fn": fn, @@ -277,20 +300,23 @@ return meta # Unpack config: structures # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Further breaks up the meta['config'] descriptor. -# Creates an array from JSON/YAML option lists. -# -# config: -# { name: 'var1', type: text, value: "default, ..." } -# { name=option2, type=boolean, $value=1, etc. } -# -# Stubs out name, value, type, description if absent. -# def plugin_meta_config(str): + """ + Further breaks up the meta['config'] descriptor. + Creates an array from JSON/YAML option lists. + + :arg str str: unprocessed config: field + + Stubs out name, value, type, description if absent. + # config: + { name: 'var1', type: text, value: "default, ..." } + { name=option2, type=boolean, $value=1, etc. } + """ + config = [] for entry in rx.config.findall(str): entry = entry[0] or entry[1] opt = { "type": None, @@ -322,17 +348,19 @@ ) # Comment extraction regexps # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Pretty crude comment splitting approach. But works -# well enough already. Technically a YAML parser would -# do better; but is likely overkill. -# class rx: + """ + Pretty crude comment splitting approach. But works + well enough already. Technically a YAML parser would + do better; but is likely overkill. + """ + comment = re.compile(r"""(^ {0,4}#.*\n)+""", re.M) - hash = re.compile(r"""(^ {0,4}# {0,3}\r*)""", re.M) + hash = re.compile(r"""(^ {0,4}#{1,2} {0,3}\r*)""", re.M) keyval = re.compile(r""" ^([\w-]+):(.*$(?:\n(?![\w-]+:).+$)*) # plain key:value lines """, re.M | re.X) config = re.compile(r""" \{ ((?: [^\{\}]+ | \{[^\}]*\} )+) \} # JSOL/YAML scheme {...} dicts @@ -351,38 +379,34 @@ # ArgumentParser options conversion # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# As variation of in-application config: options, this method converts -# cmdline argument specifiers. -# -# config: -# { arg: -i, name: input[], type: str, description: input files } -# -# Which allows to collect argumentparser options from different plugins. -# The only difference to normal config entries is the `arg:` attribute. -# -# · It adds array arguments with a [] name suffix, or a `*` type suffix. -# Else even a `?` or `+` and numeric counts after the type flag. -# -# · Understands the types `str`, `int` and `bool`. -# -# · Entries may carry a `hidden: 1` or `required: 1` attribute. -# -# · And `help:` is an alias to `description:` -# And `default:` an alias for `value:` -# -# · While `type: select` utilizes the `select: a|b|c` format as usual. -# -# ArgParsers const=, metavar= flag, or type=file are not aliased here. -# -# Basically returns a dictionary that can be fed per **kwargs directly -# to an ArgumentParsers add_argument(). Iterate over each plugins -# meta['config'][] options to convert them. -# def argparse_map(opt): + """ + As variation of in-application config: options, this method converts + cmdline argument specifiers. + + # config: + { arg: -i, name: input[], type: str, description: input files } + + Which allows to collect argumentparser options from different plugins. + The only difference to normal config entries is the `arg:` attribute. + + · It adds array arguments with a [] name suffix, or a `*` type suffix. + Else even a `?` or `+` and numeric counts after the type flag. + · Understands the types `str`, `int` and `bool`. + · Entries may carry a `hidden: 1` or `required: 1` attribute. + · And `help:` is an alias to `description:` + And `default:` an alias for `value:` + · While `type: select` utilizes the `select: a|b|c` format as usual. + + ArgParsers const=, metavar= flag, or type=file are not aliased here. + Basically returns a dictionary that can be fed per **kwargs directly + to an ArgumentParsers add_argument(). Iterate over each plugins + meta['config'][] options to convert them. + """ if not ("arg" in opt and opt["name"] and opt["type"]): return {} # Extract --flag names args = opt["arg"].split() + re.findall(r"-+\w+", opt["name"]) @@ -397,10 +421,11 @@ false_b = "false" in typing or opt["value"] in ("0", "false") # print("\nname=", name, "is_arr=", is_arr, "is_bool=", is_bool, # "bool_d=", false_b, "naming=", naming, "typing=", typing) # Populate combination as far as ArgumentParser permits + # pylint: disable=bad-whitespace, bad-continuation kwargs = dict( args = args, dest = name[0] if not name[0] in args else None, action = is_arr and "append" or is_bool and false_b and "store_false" @@ -414,181 +439,53 @@ help = opt["description"] if not "hidden" in opt else argparse.SUPPRESS ) return {k: w for k, w in kwargs.items() if w is not None} - -# Minimal depends: probing -# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Now this definitely requires customization. Each plugin can carry -# a list of (soft-) dependency names. -# -# depends: config, appcore >= 2.0, bin:wkhtmltoimage, python < 3.5 -# -# Here only in-application modules are honored, system references -# ignored. Unknown plugin names are also skipped. A real install -# helper might want to auto-tick them on, etc. This example is just -# meant for probing downloadable plugins. -# -# The .valid() helper only asserts the api: string, or skips existing -# modules, and if they're more recent. -# While .depends() compares minimum versions against existing modules. -# -# In practice there's little need for full-blown dependency resolving -# for application-level modules. -# -class dependency(object): - - # prepare list of known plugins and versions - def __init__(self, add={}, core=["st2", "uikit", "config", "action"]): - self.have = { - "python": { "version": sys.version } - } - # inject virtual modules - for name, meta in add.items(): - if isinstance(meta, bool): meta = 1 if meta else -1 - if isinstance(meta, tuple): meta = ".".join(str(n) for n in meta) - if isinstance(meta, (int, float, str)): meta = {"version": str(meta)} - self.have[name] = meta - # read plugins/* - self.have.update(all_plugin_meta()) - # add core modules - for name in core: - self.have[name] = plugin_meta(module=name, extra_base=["config"]) - # aliases - for name, meta in self.have.copy().items(): - if meta.get("alias"): - for alias in re.split(r"\s*[,;]\s*", meta["alias"]): - self.have[alias] = self.have[name] - - # basic plugin pre-screening (skip __init__, filter by api:, - # exclude installed & same-version plugins) - def valid(self, newpl, _log=lambda *x:0): - id = newpl.get("$name", "__invalid") - have_ver = self.have.get(id, {}).get("version", "0") - if id.find("__") == 0: - _log("wrong id") - pass - elif newpl.get("api") not in ("python", "streamtuner2"): - _log("wrong api") - pass - elif set((newpl.get("status"), newpl.get("priority"))).intersection(set(("obsolete", "broken"))): - _log("wrong status") - pass - elif have_ver >= newpl.get("version", "0.0"): - _log("newer version already installed") - pass - else: - return True - - # Verify depends: and breaks: against existing plugins/modules - def depends(self, plugin, _log=lambda *x:0): - r = True - if plugin.get("depends"): - r &= self.and_or(self.split(plugin["depends"]), self.have) - if plugin.get("breaks"): - r &= self.neither(self.split(plugin["breaks"]), self.have) - _log(r) - return r - - # Split trivial "pkg | alt, mod >= 1, uikit < 4.0" string into nested list [[dep],[alt,alt],[dep]] - def split(self, dep_str): - dep_cmp = [] - for alt_str in re.split(r"\s*[,;]+\s*", dep_str): - alt_cmp = [] - # split alternatives | - for part in re.split(r"\s*\|+\s*", alt_str): - # skip deb:pkg-name, rpm:name, bin:name etc. - if not len(part): - continue - if part.find(":") >= 0: - self.have[part] = { "version": self.module_test(*part.split(":")) } - # find comparison and version num - part += " >= 0" - m = re.search(r"([\w.:-]+)\s*\(?\s*([>=<!~]+)\s*([\d.]+([-~.]\w+)*)", part) - if m and m.group(2): - alt_cmp.append([m.group(i) for i in (1, 2, 3)]) - if alt_cmp: - dep_cmp.append(alt_cmp) - return dep_cmp - - # Single comparison - def cmp(self, d, have, absent=True): - name, op, ver = d - # absent=True is the relaxed check, will ignore unknown plugins // set absent=False or None for strict check (as in breaks: rule e.g.) - if not have.get(name, {}).get("version"): - return absent - # curr = installed version - curr = have[name]["version"] - tbl = { - ">=": curr >= ver, - "<=": curr <= ver, - "==": curr == ver, - ">": curr > ver, - "<": curr < ver, - "!=": curr != ver, - } - r = tbl.get(op, True) - #print "log.VERSION_COMPARE: ", name, " → (", curr, op, ver, ") == ", r - return r - - # Compare nested structure of [[dep],[alt,alt]] - def and_or(self, deps, have, r = True): - #print deps - return not False in [True in [self.cmp(d, have) for d in alternatives] for alternatives in deps] - - # Breaks/Conflicts: check [[or],[or]] - def neither(self, deps, have): - return not True in [self.cmp(d, have, absent=None) for cnd in deps for d in cnd] - - # Resolves/injects complex "bin:name" or "python:name" dependency URNs - def module_test(self, type, name): - return "1" # disabled for now - if "_" + type in dir(self): - return "1" if bool(getattr(self, "_" + type)(name)) else "-1" - # `bin:name` lookup - def _bin(self, name): - return find_executable(name) - # `python:module` test - def _python(self, name): - return __import__("imp").find_module(name) is not None - # Add plugin defaults to conf.* store # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# Utility function which applies plugin meta data to a config -# store. Which in the case of streamtuner2 is really just a -# dictionary `conf{}` and a plugin list in `conf.plugins{}`. -# -# Adds each default option value: to conf_options{}. And sets -# first plugin state (enabled/disabled) in conf_plugins{} list, -# depending on priority: classifier. -# -def add_plugin_defaults(conf_options, conf_plugins, meta={}, module=""): +def add_plugin_defaults(conf_options, conf_plugins, meta, module=""): + """ + Utility function which collect defaults from plugin meta data to + a config store. Which in the case of streamtuner2 is really just a + dictionary `conf{}` and a plugin list in `conf.plugins{}`. + + :arg dict conf_options: storage for amassed options + :arg dict conf_plugins: enable status based on plugin state/priority: + :arg dict meta: input plugin meta data (invoke once per plugin) + :arg str module: module name of meta: block + """ # Option defaults, if not yet defined for opt in meta.get("config", []): - if "name" in opt and "value" in opt: - _value = opt.get("value", "") - _name = opt.get("name") - _type = opt.get("type") - if _name not in conf_options: - # typemap - if _type == "bool": - val = _value.lower() in ("1", "true", "yes", "on") - elif _type == "int": - val = int(_value) - elif _type in ("table", "list"): - val = [ re.split(r"\s*[,;]\s*", s.strip()) for s in re.split(r"\s*[|]\s*", _value) ] - elif _type == "dict": - val = dict([ re.split(r"\s*(?:=>+|==*|-+>|:=+)\s*", s.strip(), 1) for s in re.split(r"\s*[|;,]\s*", _value) ]) - else: - val = str(_value) - conf_options[_name] = val + if "name" not in opt or "value" not in opt: + continue + _value = opt.get("value", "") + _name = opt.get("name") + _type = opt.get("type") + if _name in conf_options: + continue + # typemap + if _type == "bool": + val = _value.lower() in ("1", "true", "yes", "on") + elif _type == "int": + val = int(_value) + elif _type in ("table", "list"): + val = [ + re.split(r"\s*[,;]\s*", s.strip()) + for s in re.split(r"\s*[|]\s*", _value) + ] + elif _type == "dict": + val = dict([ + re.split(r"\s*(?:=>+|==*|-+>|:=+)\s*", s.strip(), 1) + for s in re.split(r"\s*[|;,]\s*", _value) + ]) + else: + val = str(_value) + conf_options[_name] = val # Initial plugin activation status if module and module not in conf_plugins: conf_plugins[module] = meta.get("priority") in ( "core", "builtin", "always", "default", "standard" ) - - ADDED pluginconf/depends.py Index: pluginconf/depends.py ================================================================== --- pluginconf/depends.py +++ pluginconf/depends.py @@ -0,0 +1,190 @@ +# encoding: utf-8 +# api: pluginconf +##type: class +# category: config +# title: Dependency verification +# description: Check depends: lines +# depends: pluginconf >= 0.7 +# version: 0.5 +# state: beta +# license: PD +# priority: optional +# +# This is a rather basic depends: checker, mostly for local and +# installable modules. It's largely built around streamtuner2 +# requirements, and should be customized. +# +# DependencyValidation().depends()/.valid() +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# Probes a new plugins` depends: list against installed base modules. +# Utilizes each version: fields and allows for virtual modules, or +# alternatives and honors alias: names. +# + + +import pluginconf +import re +try: + from distutils.spawn import find_executable +except ImportError: + try: + from compat2and3 import find_executable + except ImportError: + def find_executable(name): + pass +import zipfile +import logging + + +# Minimal depends: probing +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +class DependencyValidation(object): + """ + Now this definitely requires customization. Each plugin can carry + a list of (soft-) dependency names. + + # depends: config, appcore >= 2.0, bin:wkhtmltoimage, python < 3.5 + + Here only in-application modules are honored, system references + ignored. Unknown plugin names are also skipped. A real install + helper might want to auto-tick them on, etc. This example is just + meant for probing downloadable plugins. + + The .valid() helper only asserts the api: string, or skips existing + modules, and if they're more recent. + While .depends() compares minimum versions against existing modules. + + In practice there's little need for full-blown dependency resolving + for application-level modules. + """ + + """ supported APIs """ + api = ["python", "streamtuner2"] + + """ debugging """ + log = logging.getLogger("pluginconf.dependency") + + # prepare list of known plugins and versions + def __init__(self, add={}, core=["st2", "uikit", "config", "action"]): + self.have = { + "python": {"version": sys.version} + } + + # inject virtual modules + for name, meta in add.items(): + if isinstance(meta, bool): + meta = 1 if meta else -1 + if isinstance(meta, tuple): + meta = ".".join(str(n) for n in meta) + if isinstance(meta, (int, float, str)): + meta = {"version": str(meta)} + self.have[name] = meta + + # read plugins/* + self.have.update(all_plugin_meta()) + + # add core modules + for name in core: + self.have[name] = plugin_meta(module=name, extra_base=["config"]) + + # aliases + for name, meta in self.have.copy().items(): + if meta.get("alias"): + for alias in re.split(r"\s*[,;]\s*", meta["alias"]): + self.have[alias] = self.have[name] + + # basic plugin pre-screening (skip __init__, filter by api:, + # exclude installed & same-version plugins) + def valid(self, new_plugin): + id = new_plugin.get("$name", "__invalid") + have_ver = self.have.get(id, {}).get("version", "0") + if id.find("__") == 0: + self.log.debug("wrong/no id") + elif new_plugin.get("api") not in self.api: + self.log.debug("not in allowed APIs") + elif {new_plugin.get("status"), new_plugin.get("priority")} & {"obsolete", "broken"}: + self.log.debug("wrong status (obsolete/broken)") + elif have_ver >= new_plugin.get("version", "0.0"): + self.log.debug("newer version already installed") + else: + return True + + # Verify depends: and breaks: against existing plugins/modules + def depends(self, plugin): + result = True + if plugin.get("depends"): + result &= self.and_or(self.split(plugin["depends"]), self.have) + if plugin.get("breaks"): + result &= self.neither(self.split(plugin["breaks"]), self.have) + self.log.debug("plugin '%s' matching requirements: %i", plugin["id"], result) + return result + + # Split trivial "pkg | alt, mod>=1, uikit<4.0" string into nested list [[dep],[alt,alt],[dep]] + def split(self, dep_str): + dep_cmp = [] + for alt_str in re.split(r"\s*[,;]+\s*", dep_str): + alt_cmp = [] + # split alternatives | + for part in re.split(r"\s*\|+\s*", alt_str): + # skip deb:pkg-name, rpm:name, bin:name etc. + if not len(part): + continue + if part.find(":") >= 0: + self.have[part] = {"version": self.module_test(*part.split(":"))} + # find comparison and version num + part += " >= 0" + m = re.search(r"([\w.:-]+)\s*\(?\s*([>=<!~]+)\s*([\d.]+([-~.]\w+)*)", part) + if m and m.group(2): + alt_cmp.append([m.group(i) for i in (1, 2, 3)]) + if alt_cmp: + dep_cmp.append(alt_cmp) + return dep_cmp + + # Single comparison + def cmp(self, d, have, absent=True): + name, op, ver = d + # absent=True is the relaxed check, will ignore unknown plugins + # set absent=False or None for strict check (as in breaks: rule e.g.) + if not have.get(name, {}).get("version"): + return absent + # curr = installed version + curr = have[name]["version"] + tbl = { + ">=": curr >= ver, + "<=": curr <= ver, + "==": curr == ver, + ">": curr > ver, + "<": curr < ver, + "!=": curr != ver, + } + r = tbl.get(op, True) + #print "log.VERSION_COMPARE: ", name, " → (", curr, op, ver, ") == ", r + return r + + # Compare nested structure of [[dep],[alt,alt]] + def and_or(self, deps, have, r=True): + #print deps + return not False in [ + True in [self.cmp(d, have) for d in alternatives] for alternatives in deps + ] + + # Breaks/Conflicts: check [[or],[or]] + def neither(self, deps, have): + return not True in [ + self.cmp(d, have, absent=None) for cnd in deps for d in cnd + ] + + # Resolves/injects complex "bin:name" or "python:name" dependency URNs + def module_test(self, type, name): + return "1" # disabled for now + if "_" + type in dir(self): + return "1" if bool(getattr(self, "_" + type)(name)) else "-1" + + # `bin:name` lookup + def _bin(self, name): + return find_executable(name) + + # `python:module` test + def _python(self, name): + return __import__("imp").find_module(name) is not None +