Index: pluginconf/depends.py
==================================================================
--- pluginconf/depends.py
+++ pluginconf/depends.py
@@ -7,45 +7,48 @@
# depends: pluginconf >= 0.7
# version: 0.5
# state: beta
# license: PD
# priority: optional
+# permissive: 0.8
#
# 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()
-# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+# Check().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.
#
+""" Dependency validation and consistency checker for updates """
+
-import pluginconf
+import sys
import re
+#import zipfile
+import logging
+import pluginconf
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
+ find_executable = lambda name: False
# Minimal depends: probing
# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
-class DependencyValidation(object):
+class Check():
"""
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
+ … # 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.
@@ -54,97 +57,136 @@
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.
+
+ | Attributes | | |
+ |------------|---------|-----------------------------------------------------|
+ | api | list | allowed api: identifiers for .valid() stream checks |
+ | system_deps| bool | check `bin:app` or `python:package` dependencies |
+ | log | logging | warning handler |
+ | have | dict | accumulated list of existing/virtual plugins |
"""
-
- """ supported APIs """
+
+ # supported APIs
api = ["python", "streamtuner2"]
-
- """ debugging """
+
+ # debugging
log = logging.getLogger("pluginconf.dependency")
- # prepare list of known plugins and versions
- def __init__(self, add={}, core=["st2", "uikit", "config", "action"]):
+ # ignore bin:… or python:… package in depends
+ system_deps = False
+
+ def __init__(self, add=None, core=["st2", "uikit", "config", "action"]):
+ """
+ Prepare list of known plugins and versions in self.have={}
+
+ | Parameters | | |
+ |------------|---------|------------------------------------------------------|
+ | add | dict | name→pmd of existing/core plugins (incl ver or deps) |
+ | core | list | name list of virtual plugins |
+ """
self.have = {
"python": {"version": sys.version}
}
# inject virtual modules
- for name, meta in add.items():
+ for name, meta in (add or {}).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())
+ self.have.update(pluginconf.all_plugin_meta())
# add core modules
for name in core:
- self.have[name] = plugin_meta(module=name, extra_base=["config"])
+ self.have[name] = pluginconf.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:
+ """
+ Plugin pre-screening from online repository stream.
+ Fields are $name, $file, $dist, api, id, depends, etc
+ Exclude installed or for newer-version presence.
+
+ | Parameters | | |
+ |-------------|---------|------------------------------------------------------|
+ | new_plugin | dict | online properties of available plugin |
+ | **Returns** | bool | is updatatable |
+ """
+ if not "$name" in new_plugin:
+ self.log.warning(".valid() checks online plugin lists, requires $name")
+ name = new_plugin.get("$name", "__invalid")
+ have_ver = self.have.get(name, {}).get("version", "0")
+ if name.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
+ return False
- # Verify depends: and breaks: against existing plugins/modules
def depends(self, plugin):
+ """
+ Verify depends: and breaks: against existing plugins/modules
+
+ | Parameters | | |
+ |-------------|---------|------------------------------------------------------|
+ | plugin | dict | plugin meta properties of (new?) plugin |
+ | **Returns** | bool | matches up with existing .have{} installation |
+ """
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):
+ """
+ Split trivial "pkg | alt, mod>=1, uikit<4.0" string
+ into nested list [ [alt, alt], [dep], [dep] ];
+ with each entry comprised of (name, operator, version).
+ """
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):
+ if not 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*([>==": curr > ver,
"<": curr < ver,
"!=": curr != ver,
}
- r = tbl.get(op, True)
- #print "log.VERSION_COMPARE: ", name, " → (", curr, op, ver, ") == ", r
- return r
+ result = tbl.get(operator, True)
+ self.log.debug("VERSION_COMPARE: %s → (%s %s %s) == %s", name, curr, operator, ver, result)
+ return result
- # Compare nested structure of [[dep],[alt,alt]]
- def and_or(self, deps, have, r=True):
+ def and_or(self, deps, have, inner_true=True):
+ """ Compare nested structure of [[dep],[alt,alt]] """
#print deps
return not False in [
- True in [self.cmp(d, have) for d in alternatives] for alternatives in deps
+ inner_true in [self.cmp(d, have) for d in alternatives] for alternatives in deps
]
- # Breaks/Conflicts: check [[or],[or]]
def neither(self, deps, have):
+ """ Breaks/Conflicts: check [[or],[or]] """
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):
+ def module_test(self, urn, name):
+ """ Probes "bin:name" or "python:name" dependency URNs """
+ if not self.system_deps:
+ return "1"
+ if "_" + urn in dir(self):
+ if bool(getattr(self, "_" + urn)(name)):
+ return "1"
+ return "-1" # basically a negative version -v1
+
+ @staticmethod
+ def _bin(name):
+ """ `bin:name` lookup """
return find_executable(name)
- # `python:module` test
- def _python(self, name):
+ @staticmethod
+ def _python(name):
+ """ `python:module` test """
return __import__("imp").find_module(name) is not None
-
Index: pluginconf/pluginconf.py
==================================================================
--- pluginconf/pluginconf.py
+++ pluginconf/pluginconf.py
@@ -2,22 +2,31 @@
# api: python
##type: extract
# category: config
# title: Plugin configuration
# description: Read meta data, pyz/package contents, module locating
-# version: 0.7.7
+# version: 0.8.1
# state: stable
# classifiers: documentation
+# depends: python >= 2.7
+# suggests: python:flit, python:PySimpleGUI
# license: PD
# priority: core
+# api-docs: https://fossil.include-once.org/pluginspec/doc/trunk/html/index.html
# docs: https://fossil.include-once.org/pluginspec/
-# url: http://fossil.include-once.org/streamtuner2/wiki/plugin+meta+data
+# url: https://fossil.include-once.org/pluginspec/wiki/pluginconf
# config: -
+# format: off
+# permissive: 0.75
+# pylint: disable=invalid-name
+# console-scripts: flit-pluginconf=pluginconf.flit:main
#
# Provides plugin lookup and meta data extraction utility functions.
# It's used to abstract module+option management in applications.
# For consolidating internal use and external/tool accessibility.
+# Generally these functions are highly permissive / error tolerant,
+# to preempt initialization failures for applications.
#
# The key:value format is language-agnostic. It's basically YAML in
# a topmost script comment. For Python only # hash comments though.
# Uses common field names, a documentation block, and an obvious
# `config: { .. }` spec for options and defaults.
@@ -32,10 +41,13 @@
# plugin_meta()
# ‾‾‾‾‾‾‾‾‾‾‾‾‾
# Is the primary function to extract a meta dictionary from files.
# It either reads from a given module= name, a literal fn=, or just
# src= code, and as fallback inspects the last stack frame= else.
+#
+# The resulting dict allows [key] and .key access. The .config
+# list further access by option .name.
#
# module_list()
# ‾‾‾‾‾‾‾‾‾‾‾‾‾
# Returns basenames of available/installed plugins. It uses the
# plugin_base=[] list for module relation. Which needs to be set up
@@ -58,29 +70,50 @@
# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾
# Converts a list of config: options with arg: attribute for use as
# argparser parameters.
#
#
-# 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
-# lookup via pkgutil.iter_modules().
-#
-# And a central module can be extended with new lookup locations best
-# by attaching new locations itself via module.__path__ + ["./local"]
-# for example.
-#
-# Plugin loading thus becomes as simple as __import__("ext.local").
-# The attached plugin_state config dictionary in most cases can just
-# list module basenames, if there's only one set to manage.
+# Simple __import__() scheme
+# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+# Generally this scheme concerns itself more with plugin basenames.
+# That is: module scripts in a package like `plugins.plg1`. To do so,
+# have an `plugins/__init__.py` which sets its own `__path__`.
+# Inject that package name into `plugin_base = ["plugins"]`. Thus
+# any associated paths can be found per pkgutil.iter_modules().
+#
+# Importing modules then also becomes as simple as invoking
+# `module = __import__(f"plugins.{basename}"]` given a plugin name.
+# The "plugins" namespace can subsequently be expanded by attaching
+# more paths, such as `+= ["./config/usermodules"]` or similiar.
+#
+# Thus a plugin_state config dictionary in most cases can just list
+# module basenames, if there's only one namespace to manage. (Plugin
+# names unique across application.)
+
+"""
+Plugin meta extraction and module lookup.
+
+
+
+
+
+
Main function plugin_meta() unpacks meta fields
+ into dictionaries.
+
Other utility code is about module listing, relative to
+ plugin_base anchors.
+