ADDED LICENSE Index: LICENSE ================================================================== --- LICENSE +++ LICENSE @@ -0,0 +1,1 @@ +Publid Domain ADDED README.md Index: README.md ================================================================== --- README.md +++ README.md @@ -0,0 +1,189 @@ +Provides meta data extraction and plugin basename lookup. And it’s meant for +in-application feature and option management. +The [descriptor format](https://fossil.include-once.org/pluginspec/index) +(*self-contained* atop each script) is basically: + + # encoding: utf-8 + # api: python + # type: handler + # category: io + # title: Plugin configuration + # description: Read meta data, pyz/package contents, module locating + # version: 0.5 + # priority: core + # docs: http://fossil.include-once.org/streamtuner2/wiki/plugin+meta+data + # config: + # { name: xyz, value: 1, type: bool, description: "Sets..." } + # { name: var, value: "init", description: "Another..." } + # license: MITL + # + # Documentation goes here... + +The `key: value` format is language-agnostic. It’s basically YAML in the +topmost script comment. For Python only # hash comments are used. Defaults +to rather common field names, encourages a documentation block, and an +obvious [config: { .. } scheme](https://fossil.include-once.org/pluginspec/wiki/config) +for options and defaults. + +How it's used: + + import pluginconf + meta = pluginconf.plugin_meta(fn="./plugin/func.py") + print(meta) + +What it’s not: + +> * This is not another config reader/parser/storage class. +> * Doesn’t impose a specific plugin API. +> * Neither concerns itself with module/package loading. (See [pluginbase](https://pypi.org/project/pluginbase/) or just `__import__`.) + +What for then? + +> * Separates code from meta data. Avoids keeping seldomly used descriptors in variables. +> * Does away with externalized ini/json files for modules, yet simplifies use of external tooling. +> * Minimizes premature module loading just to inspect meta information. + +pluginconf is less about a concrete implementation/code, but pushing a universal meta data format. + + +# API + +Lookup configuration is currently just done through injection: + + plugin_base = [__package__, "myapp.plugins", "/usr/share/app/extensions"] + module_base = "pluginconf" # or any top-level app module + +Which declares module and plugin basenames, which get used for lookups by +just module= names in e.g. `module_list()`. (Works for literal setups +and within PYZ bundles). +This is unnecessary for plain `plugin_meta(fn=)` extraction. + +#### plugin_meta( module= | fn= | src= | frame= ) + +Returns a meta data dictionary for the given module name, file, source code, or caller frame: + + { + "title": "Compound★", + "description": "...", + "version": "0.1", + "type": "channel", + "category": "virtual", + "config": […], + "doc": "The multiline comment \n following meta fields..." + … + } + +And that’s already all it does. +All other methods in pluginconf are mostly just for module lookup or data +retrieval. + +#### module_list() + +Returns basenames of available/installed plugins (from possible sources +in `plugin_base`). + +#### add_plugin_defaults() + +Populates your config_options{} and plugin_states{} list. Can be a classic dict, +or one of the hundreds of config parser/managers. You might want to combine +config options and plugin states in a single dict even: + + import pluginconf + pluginconf.module_base = __name__ + pluginconf.plugin_base = [__package__] + + conf = { + "defaults": "123", + "plugins": {} # ← stores the activation states + } + + for module,meta in pluginconf.all_plugin_meta().items(): + pluginconf.add_plugin_defaults(conf, conf["plugins"], meta, module) + # share the same dict ↑ ↑ + +#### get_data( fn= ) + +Is mostly an alias for pkgutil.get_data(). Abstracts usage within PYZ packages a little. + +#### argparse_map() + +Provides a simpler way to specify ugly argparse definitions. And allows to amass options from plugins. + +# GUI + +There's a Tkinter/PySimpleGUI variant of the option dialog from +[streamtuner2](https://fossil.include-once.org/streamtuner2/) included: + + +The `pluginconf.gui.window()` implementation has a few less features, but +might suffice for common applications. It just lists a single pane of +settings, and doesn't even attempt to group by categories. + +Its main function performs the plugin lookup (`*.py` meta reading) and +displays an editing window: + + import pluginconf.gui + config = { + "debug": 0, "verbose": 1, "temp_dir": "/tmp" + } + plugin_states = { + "core": 1, "printing_ui": 0 + } + pluginconf.gui.window(config, plugin_states, files=["./library/*.py"]) + +Where both `config` and `plugin_states` get updated after invocation. The +function return value indicates whether save or cancel was pressed. + + * `plugin_states={}` can be omitted/empty, but the GUI will still display + checkboxes for plugin files, even if they go unused. + * Supports only basic option types (bool, str, int, select), no table/dict. + * Type casting is somewhat basic (no integer regex). + * And doesn't support nested config names=`app[module][var]` yet. + * The config dict might be prefilled from either in-app defaults, + or `json.load()`, and/or per `pluginconf.add_plugin_defaults()`. + * It's still up to the application how/where to store the config{} dict (e.g. `json.dumps()`). + * And alternatively to the *.py glob list, you could inject a prepared + dictionary as `plugins={}` list (keys are unused) and leave `files=None`. + * Any PySimpleGUI options (title=, theme=, resizable=) are passed through to + the config window. + +Overall it's surprisingly short given the PySimpleGUI result set. It would +likely behave as well, if e.g. additional tabs or input widgets were added. + + +# setup.py wrapper + +Another obvious use case for PMD is simplifying packaging. A `setup()` +script can become as short as: + + from pluginconf.setup import setup + setup( + fn="main/pkg.py" + ) + +Which will reuse version: and descriptors from the meta comment. For simple +one-module packages, you might get away with just `setup()` and an all +automatic lookup. The non-standard PMD field `# classifiers: x11, python` +can be used to lookup trove categories (crude search on select topics). +All other `setup(fields=…)` are passed on to distutils/setuptools as is. +-- Btw, [setupmeta](https://pypi.org/project/setupmeta/) is an even more +versatile wrapper with sensible defaults and source scanning. + + +#### Caveats + + * It’s mostly just an excerpt from streamtuner2. + * Might need customization prior use. + * The GUI implmentation is fairly simplistic. + * Doesn't bundle any plugin repo loader logic. + * So doesn't make use of the dependency class. + * The description fields can double as packaging source (setup.py). There's also a + [# pack: specifier](https://fossil.include-once.org/pluginspec/wiki/References) + for fpm (deb/rpm/arch/exe/pyzw/pip generation), unused in the `setup.py` + wrapper here however. + * In Python `# type:` might neede doubled commenting (## pylint), or alternating to + other descriptors like`class:` or `slot:`. (The whole scheme is agnostic to + designators. Common keys are just recommended for potential interoparability.) + * The format incidentally mixes well with established comment markers like + `# format: off` or `# pylint: disable=…` for instance. + ADDED TODO Index: TODO ================================================================== --- TODO +++ TODO @@ -0,0 +1,2 @@ + - argparse + - pmd2inks ADDED g.py Index: g.py ================================================================== --- g.py +++ g.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +import pluginconf.gui +pluginconf.gui.window({}, {}, ["../streamtuner2/channels/*.py"]) ADDED pluginconf/__init__.py Index: pluginconf/__init__.py ================================================================== --- pluginconf/__init__.py +++ pluginconf/__init__.py @@ -0,0 +1,594 @@ +# encoding: utf-8 +# api: python +# type: extract +# category: config +# title: Plugin configuration +# description: Read meta data, pyz/package contents, module locating +# version: 0.7.7 +# state: stable +# classifiers: documentation +# license: PD +# priority: core +# docs: https://fossil.include-once.org/pluginspec/ +# url: http://fossil.include-once.org/streamtuner2/wiki/plugin+meta+data +# config: - +# +# 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. +# +# 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. +# +# It neither imposes a specific module/plugin API, nor config storage, +# and doesn't fixate module loading. It's really just meant to look +# up meta infos. +# This approach avoids in-code values/inspection, externalized meta +# descriptors, and any hodgepodge or premature module loading just to +# uncover module description fields. +# +# 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. +# +# module_list() +# ‾‾‾‾‾‾‾‾‾‾‾‾‾ +# Returns basenames of available/installed plugins. It uses the +# plugin_base=[] list for module relation. Which needs to be set up +# beforehand, or injected. +# +# add_plugin_defaults() +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# Populates a config_options{} and plugin_states{} list. Used for +# initial setup, or when adding new plugins, etc. Both dicts might +# also be ConfigParser stores, or implement magic __set__ handling +# to act on state changes. +# +# get_data() +# ‾‾‾‾‾‾‾‾‾‾ +# Is mostly an alias for pkgutil.get_data(). It abstracts the main +# base path, allows PYZ usage, and adds some convenience flags.‾ +# It's somewhat off-scope for plugin management, but used internally. +# +# 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 +# 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. + + +import sys +import os +import re +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: + 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 +import zipfile +import argparse + +__all__ = [ + "get_data", "module_list", "plugin_meta", + "dependency", "add_plugin_defaults" +] + + +# Injectables +# ‾‾‾‾‾‾‾‾‾‾‾ +log_ERR = lambda *x: None + +# File lookup relation for get_data(), should name a top-level package. +module_base = "config" # equivalent PluginBase(package=…) + +# 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 + + +# 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): + try: + bin = pkgutil.get_data(file_base or module_base, fn) + if gz: + bin = gzip_decode(bin) + if decode: + return bin.decode("utf-8", errors='ignore') + else: + return str(bin) + 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=[]): + + # Convert plugin_base package names into paths for iter_modules + paths = [] + for mp in plugin_base: + if sys.modules.get(mp): + 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) + 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(): + 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): + + # Try via pkgutil first, + # find any plugins.* modules, or main packages + if module: + fn = module + for base in plugin_base + 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) + + # Else get source directly from caller + elif not src and not fn: + module = inspect.getmodule(sys._getframe(frame)) + fn = inspect.getsourcefile(module) + src = inspect.getcomments(module) + + # Assume it's a filename within a zip + elif fn: + intfn = "" + while fn and len(fn) and not os.path.exists(fn): + fn, add = os.path.split(fn) + intfn = add + "/" + intfn + if len(fn) >= 3 and intfn and zipfile.is_zipfile(fn): + src = zipfile.ZipFile(fn, "r").read(intfn.strip("/")) + + # Extract source comment into meta dict + if not src: + src = "" + if not isinstance(src, str): + src = src.decode("utf-8", errors='replace') + 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): + + # Defaults + meta = { + "id": os.path.splitext(os.path.basename(fn or ""))[0], + "fn": fn, + "api": "python", + "type": "module", + "category": None, + "priority": None, + "version": "0", + "title": fn, + "description": "no description", + "config": [], + "doc": "" + } + + # Extract coherent comment block + src = src.replace("\r", "") + if not literal: + src = rx.comment.search(src) + if not src: + log_ERR("Couldn't read source meta information:", fn) + return meta + src = src.group(0) + src = rx.hash.sub("", src).strip() + + # Split comment block + if src.find("\n\n") > 0: + src, meta["doc"] = src.split("\n\n", 1) + + # Turn key:value lines into dictionary + for field in rx.keyval.findall(src): + meta[field[0].replace("-", "_")] = field[1].strip() + meta["config"] = plugin_meta_config(meta.get("config") or "") + + 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): + config = [] + for entry in rx.config.findall(str): + entry = entry[0] or entry[1] + opt = { + "type": None, + "name": None, + "description": "", + "value": None + } + for field in rx.options.findall(entry): + opt[field[0]] = (field[1] or field[2] or field[3] or "").strip() + # normalize type + opt["type"] = config_opt_type_map.get(opt["type"], opt["type"] or "str") + # preparse select: + if opt.get("select"): + opt["select"] = config_opt_parse_select(opt.get("select", "")) + config.append(opt) + return config + +# split up `select: 1=on|2=more|3=title` or `select: foo|bar|lists` +def config_opt_parse_select(s): + if re.search("([=:])", s): + return dict(rx.select_dict.findall(s)) + else: + return dict([(v, v) for v in rx.select_list.findall(s)]) + +# normalize type:names to `str`, `text`, `bool`, `int`, `select`, `dict` +config_opt_type_map = dict( + longstr="text", string="str", boolean="bool", checkbox="bool", integer="int", number="int", + choice="select", options="select", table="dict", array="dict" +) + + +# 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: + comment = re.compile(r"""(^ {0,4}#.*\n)+""", re.M) + hash = re.compile(r"""(^ {0,4}# {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 + | \< (.+?) \> # old <input> HTML style + """, re.X) + options = re.compile(r""" + ["':$]? (\w*) ["']? # key or ":key" or '$key' + \s* [:=] \s* # "=" or ":" + (?: " ([^"]*) " + | ' ([^']*) ' # "quoted" or 'singl' values + | ([^,]*) # or unquoted literals + ) + """, re.X) + select_dict = re.compile(r"(\w+)\s*[=:>]+\s*([^=,|:]+)") + select_list = re.compile(r"\s*([^,|;]+)\s*") + + + +# 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): + 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"]) + + # Prepare mapping options + typing = re.findall(r"bool|str|\[\]|const|false|true", opt["type"]) + naming = re.findall(r"\[\]", opt["name"]) + name = re.findall(r"(?<!-)\b\w+", opt["name"]) + nargs = re.findall(r"\b\d+\b|[\?\*\+]", opt["type"]) or [None] + is_arr = "[]" in (naming + typing) and nargs == [None] + is_bool = "bool" in typing + 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 + 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" + or is_bool and "store_true" or "store", + nargs = nargs[0], + default = opt.get("default") or opt["value"], + type = None if is_bool else ("int" in typing and int + or "bool" in typing and bool or str), + choices = opt["select"].split("|") if "select" in opt else None, + required = "required" in opt or None, + 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=""): + + # 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 + + # 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/gui.py Index: pluginconf/gui.py ================================================================== --- pluginconf/gui.py +++ pluginconf/gui.py @@ -0,0 +1,202 @@ +# encoding: UTF-8 +# api: python +# type: ui +# category: io +# title: Config GUI +# description: Display plugins + options in setup window +# version: 0.8 +# depends: python:pysimplegui (>= 4.0) +# priority: optional +# config: - +# +# Creates a PySimpleGUI options list. Scans a given list of *.py files +# for meta data, then populates a config{} dict and (optionally) a state +# map for plugins themselves. +# +# jsoncfg = {} +# pluginconf.gui.window(jsoncfg, {}, ["plugins/*.py"]) +# +# Very crude, and not as many widgets as the Gtk/St2 implementation. +# Supports type: str, bool, select, int, dict, text config: options. +# + + +import PySimpleGUI as sg +import pluginconf +import glob, json, os, re, textwrap + + +# temporarily store collected plugin config: dicts +options = {} + + +#-- show configuation window +def window(config={}, plugin_states={}, files=["*/*.py"], plugins={}, opt_label=False, theme="DefaultNoMoreNagging", **kwargs): + """ + Reads *.py files and crafts a settings dialog from meta data. + + Parameters + ---------- + config : dict + Config settings, updated after dialog completion + plugin_states : dict + Plugin activation states, also input/output + files : list + Glob list of *.py files to extract meta definitions from + plugins : dict + Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected + opt_label : bool + Show config name= as label + **kwargs : dict + Other options are passed on to PySimpleGUI + """ + + if theme: + sg.theme(theme) + if files: + plugins = read_options(files) + layout = plugin_layout(plugins.values(), config, plugin_states, opt_label=opt_label) + layout.append([sg.T(" ")]) + #print(repr(layout)) + + # pack window + layout = [ + [sg.Column(layout, expand_x=1, expand_y=0, size=(575,680), scrollable="vertically", element_justification='left')], + [sg.Column([[sg.Button("Cancel"), sg.Button("Save")]], element_justification='right')] + ] + if not "title" in kwargs: + kwargs["title"] = "Options" + if not "font" in kwargs: + kwargs["font"] = "Sans 11" + win = sg.Window(layout=layout, resizable=1, **kwargs) + + # wait for save/exit + event,data = win.read() + win.close() + if event=="Save": + for k,v in data.items(): + if options.get(k): + #@ToDo: handle array[key] names + config[k] = cast.fromtype(data[k], options[k]) + elif type(k) is str and k.startswith('p:'): + k = k.replace('p:', '') + if plugins.get(k): + plugin_states[k] = v + return True + #print(config, plugin_states) + + +# craft list of widgets for each read plugin +def plugin_layout(ls, config, plugin_states, opt_label=False): + layout = [] + for plg in ls: + #print(plg.get("id")) + layout = layout + plugin_entry(plg, plugin_states) + for opt in plg["config"]: + if opt.get("name"): + if opt_label: + layout.append([sg.T(opt["name"], font=("Sans",11,"bold"), pad=((50,0),(7,0)))]) + layout.append(option_entry(opt, config)) + return layout + +# checkbox for plugin name +def plugin_entry(e, plugin_states): + id = e["id"] + return [ + [ + sg.Checkbox( + e.get("title", id), key='p:'+id, default=plugin_states.get(id, 0), tooltip=e.get("doc"), metadata="plugin", + font="bold", pad=(0,(8,0)) + ), + sg.Text("({}/{})".format(e.get("type"), e.get("category")), text_color="#005", pad=(0,(8,0))), + sg.Text(e.get("version"), text_color="#a72", pad=(0,(8,0))) + ], + [ + sg.Text(e.get("description", ""), tooltip=e.get("doc"), font=("sans", 10), pad=(26,(0,10))) + ] + ] + +# widgets for single config option +def option_entry(o, config): + #print(o) + name = o.get("name", "") + desc = wrap(o.get("description", name), 60) + type = o.get("type", "str") + help = o.get("help", None) + if help: + help = wrap(help, 60) + options[name] = o + val = config.get(name, o.get("value", "")) + if o.get("hidden"): + pass + elif type == "str": + return [ + sg.InputText(key=name, default_text=str(val), size=(20,1), pad=((50,0),3)), + sg.Text(wrap(desc, 50), pad=(5,2), tooltip=help or name, justification='left', auto_size_text=1) + ] + elif type == "text": + return [ + sg.Multiline(key=name, default_text=str(val), size=(45,4), pad=((40,0),3)), + sg.Text(wrap(desc, 20), pad=(5,2), tooltip=help or name, justification='left', auto_size_text=1) + ] + elif type == "bool": + return [ + sg.Checkbox(wrap(desc, 70), key=name, default=cast.bool(val), tooltip=help or name, pad=((40,0),2), auto_size_text=1) + ] + elif type == "int": + return [ + sg.InputText(key=name, default_text=str(val), size=(6,1), pad=((50,0),3)), + sg.Text(wrap(desc, 60), pad=(5,2), tooltip=help or name, auto_size_text=1) + ] + elif type == "select": + #o["select"] = parse_select(o.get("select", "")) + values = [v for v in o["select"].values()] + return [ + sg.Combo(key=name, default_value=o["select"].get(val, val), values=values, size=(15,1), pad=((50,0),0), font="Sans 11"), + sg.Text(wrap(desc, 47), pad=(5,2), tooltip=help or name, auto_size_text=1) + ] + elif type == "dict": # or "table" rather ? + return [ + sg.Table(values=config.get(name, ["", ""]), headings=o.get("columns", "Key,Value").split(","), + num_rows=5, col_widths=30, def_col_width=30, auto_size_columns=False, max_col_width=150, key=name, tooltip=help or desc) + ] + return [] + + +#-- read files, return dict of {id:pmd} for all plugins +def read_options(files): + ls = [pluginconf.plugin_meta(fn=fn) for pattern in files for fn in glob.glob(pattern)] + return dict( + (meta["id"], meta) for meta in ls + ) + + +#-- map option types (from strings) +class cast: + @staticmethod + def bool(v): + if v in ("1", 1, True, "true", "TRUE", "yes", "YES", "on", "ON"): + return True + return False + @staticmethod + def int(v): + return int(v) if re.match("-?\d+", v) else 0 + @staticmethod + def fromtype(v, opt): + if not opt.get("type"): + return str(v) + elif opt["type"] == "int": + return cast.int(v) + elif opt["type"] == "bool": + return cast.bool(v) + elif opt["type"] == "select": + inverse = dict((v,k) for k,v in opt["select"].items()) + return inverse.get(v, v) + elif opt["type"] == "text": + return str(v).rstrip() + else: + return v + +#-- textwrap for `description` and `help` option fields +def wrap(s, w=50): + return "\n".join(textwrap.wrap(s, w)) if s else "" ADDED pluginconf/setup.py Index: pluginconf/setup.py ================================================================== --- pluginconf/setup.py +++ pluginconf/setup.py @@ -0,0 +1,473 @@ +# encoding: utf-8 +# api: setuptools +# type: functions +# title: setup() wrapper +# description: utilizes PMD to populate some package fields +# version: 0.3 +# license: PD +# +# Utilizes plugin meta data to cut down setup.py incantations +# to basically just: +# +# from pluginconf.setup import setup +# setup( +# fn="pluginconf/__init__.py" +# ) +# +# Where the fn= either pinpoints the main invocation point, +# when name= or packages= (implicitly from `find_packages()`) +# don't specify a locatable script already. +# +# Mapped meta fields include: +# +# # description: … +# # version: … +# # url: … +# # depends: python (>= 2.7), python:pkg-name (>= 1.0) +# # suggests: python:extras_require (>= 1.0) +# # author: … +# # license: spdx +# # state: stable +# # type: classifier +# # category: classifier +# # keywords: tag, tag +# # classifiers: tag, trove, shortcuts +# # doc-format: text/markdown +# # +# # Long description used in lieu of README... +# +# A README.* will be read if present, else PMD comment used. +# Classifiers and license matching is very crude, just for +# the most common cases. Type:, Category: and Classifiers: +# or Keywords: are also scanned for trove classifers. +# + + +import os, re, glob +import setuptools +import pluginconf + + +def _name_to_fn(name): + """ find primary entry point.py from package name """ + for pfx in "", "src/", "src/"+name+"/": + for sfx in ".py", "/__init__.py": + if os.path.exists(pfx+name+sfx): + return pfx+name+sfx + +def _get_readme(): + """ get README.md contents """ + for fn,mime in ("README.md", "text/markdown"), ("README.rst", "text/x-rst"), ("README.txt", "text/plain"): + if os.path.exists(fn): + with open(fn, "r") as f: + return f.read(), mime + #system("pandoc -f markdown -t rst README.md > README.rst") + return "", "text/plain" + +def setup(debug=0, **kwargs): + """ + Wrapper around `setuptools.setup()` which adds some defaults + and plugin meta data import, with two shortcut params: + + fn="pkg/main.py", + long_description="@README.md" + + Other setup() params work as usual. + """ + + # stub values + stub = { + "classifiers": [], + "project_urls": {}, + "python_requires": ">= 2.7", + "install_requires": [], + "extras_require": {}, + #"package_dir": {"": "."}, + #"package_data": {}, + #"data_files": [], + "entry_points": {}, + "packages": setuptools.find_packages() + } + for k,v in stub.items(): + if not k in kwargs: + kwargs[k] = v + + # package name + if not "name" in kwargs: + kwargs["name"] = kwargs["packages"][0] + + # read README + if not "long_description" in kwargs or re.match("^@?([.\w]+/)*README.(\w+)$", kwargs.get("long_description", "-")): + kwargs["long_description"], kwargs["long_description_content_type"] = _get_readme() + + # search name= package if no fn= given + if not kwargs.get("fn") and kwargs.get("name"): + kwargs["fn"] = _name_to_fn(kwargs["name"]) + + # read plugin meta data (PMD) + pmd = {} + pmd = pluginconf.plugin_meta(fn=kwargs["fn"]) + + # id: if no name= still + if pmd.get("id") and not kwargs.get("name"): + if pmd["id"] == "__init__": + pmd["id"] = re.findall("([\w\.\-]+)/__init__.+$", kwargs["fn"])[0] + kwargs["name"] = pmd["id"] + # cleanup + if "fn" in kwargs: + del kwargs["fn"] + + # version:, description:, author: + for field in "version", "description", "license", "author", "url": + if field in pmd and not field in kwargs: + kwargs[field] = pmd[field] + # other urls: + for k,url in pmd.items(): + if type(url) is str and k != "url" and re.match("https?://\S+", url): + kwargs["project_urls"][k.title()] = url + # depends: + if "depends" in pmd: + deps = re.findall("python\s*\(?(>=?\s?[\d.]+)", pmd["depends"]) + if deps: + kwargs["python_requires"] = deps[0] + if "depends" in pmd and not kwargs["install_requires"]: + deps = re.findall("(?:python|pip):([\w\-]+)\s*(\(?[<=>\s\d.\-]+)?", pmd["depends"]) + if deps: + kwargs["install_requires"] = [name+re.sub("[^<=>\d.\-]", "", ver) for name,ver in deps] + # suggests: + if "suggests" in pmd and not kwargs["extras_require"]: + deps = re.findall("(?:python|pip):([\w\-]+)\s*\(?\s*([>=<]+\s*[\d.\-]+)", pmd["suggests"]) + if deps: + kwargs["extras_require"].update(dict(deps)) + # doc: + if not kwargs.get("long_description"): + kwargs["long_description"] = pmd["doc"] + kwargs["long_description_content_type"] = pmd.get("doc_format", "text/plain") + # keywords= + if not "keywords" in kwargs: + if "keywords" in pmd: + kwargs["keywords"] = pmd["keywords"] + elif "category" in pmd: + kwargs["keywords"] = pmd["category"] + + # automatic inclusions + if not "data_files" in kwargs: + kwargs["data_files"] = [] + for man in glob.glob("man*/*.[12345678]"): + section = man[-1] + kwargs["data_files"].append(("man/man"+section, [man],)) + + # classifiers= + trove_map = { + "license": { + "MITL?": "License :: OSI Approved :: MIT License", + "PD|Public Domain": "License :: Public Domain", + "ASL": "License :: OSI Approved :: Apache Software License", + "art": "License :: OSI Approved :: Artistic License", + "BSDL?": "License :: OSI Approved :: BSD License", + "CPL": "License :: OSI Approved :: Common Public License", + "AGPL.*3": "License :: OSI Approved :: GNU Affero General Public License v3", + "AGPLv*3\+": "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "GPL": "License :: OSI Approved :: GNU General Public License (GPL)", + "GPL.*3": "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "LGPL": "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "MPL": "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Pyth": "License :: OSI Approved :: Python Software Foundation License" + }, + "state": { + "pre|release|cand": "Development Status :: 2 - Pre-Alpha", + "alpha": "Development Status :: 3 - Alpha", + "beta": "Development Status :: 4 - Beta", + "stable": "Development Status :: 5 - Production/Stable", + "mature": "Development Status :: 6 - Mature" + } + } + # license: + if pmd.get("license") and not any(re.match("License ::", l) for l in kwargs["classifiers"]): + for rx,trove in trove_map["license"].items(): + if re.search(rx, pmd["license"], re.I): + kwargs["classifiers"].append(trove) + # state: + if pmd.get("state", pmd.get("status")) and not any(re.match("Development Status ::", l) for l in kwargs["classifiers"]): + for rx,trove in trove_map["state"].items(): + if re.search(rx, pmd.get("state", pmd.get("status", "stable")), re.I): + kwargs["classifiers"].append(trove) + # topics:: + rx = "|".join(re.findall("(\w{4,})", " | ".join([pmd.get(f, "---") for f in ("api", "category", "type", "keywords", "classifiers")]))) + for line in topic_trove: + if re.search("::[^:]*("+rx+")[^:]*$", line, re.I): + if line not in kwargs["classifiers"]: + kwargs["classifiers"].append(line) + + # handover + if debug: + import pprint + pprint.pprint(kwargs) + setuptools.setup(**kwargs) + + + +topic_trove="""Topic :: Adaptive Technologies +Topic :: Artistic Software +Topic :: Communications +Topic :: Communications :: BBS +Topic :: Communications :: Chat +Topic :: Communications :: Chat :: ICQ +Topic :: Communications :: Chat :: Internet Relay Chat +Topic :: Communications :: Chat :: Unix Talk +Topic :: Communications :: Conferencing +Topic :: Communications :: Email +Topic :: Communications :: Email :: Address Book +Topic :: Communications :: Email :: Email Clients (MUA) +Topic :: Communications :: Email :: Filters +Topic :: Communications :: Email :: Mail Transport Agents +Topic :: Communications :: Email :: Mailing List Servers +Topic :: Communications :: Email :: Post-Office +Topic :: Communications :: Email :: Post-Office :: IMAP +Topic :: Communications :: Email :: Post-Office :: POP3 +Topic :: Communications :: FIDO +Topic :: Communications :: Fax +Topic :: Communications :: File Sharing +Topic :: Communications :: File Sharing :: Gnutella +Topic :: Communications :: File Sharing :: Napster +Topic :: Communications :: Ham Radio +Topic :: Communications :: Internet Phone +Topic :: Communications :: Telephony +Topic :: Communications :: Usenet News +Topic :: Database :: Database Engines/Servers +Topic :: Database :: Front-Ends +Topic :: Desktop Environment :: File Managers +Topic :: Desktop Environment :: GNUstep +Topic :: Desktop Environment :: Gnome +Topic :: Desktop Environment :: K Desktop Environment (KDE) +Topic :: Desktop Environment :: K Desktop Environment (KDE) :: Themes +Topic :: Desktop Environment :: PicoGUI +Topic :: Desktop Environment :: PicoGUI :: Applications +Topic :: Desktop Environment :: PicoGUI :: Themes +Topic :: Desktop Environment :: Screen Savers +Topic :: Documentation :: Sphinx +Topic :: Education +Topic :: Education :: Computer Aided Instruction (CAI) +Topic :: Education :: Testing +Topic :: Games/Entertainment +Topic :: Games/Entertainment :: Arcade +Topic :: Games/Entertainment :: Board Games +Topic :: Games/Entertainment :: First Person Shooters +Topic :: Games/Entertainment :: Fortune Cookies +Topic :: Games/Entertainment :: Multi-User Dungeons (MUD) +Topic :: Games/Entertainment :: Puzzle Games +Topic :: Games/Entertainment :: Real Time Strategy +Topic :: Games/Entertainment :: Role-Playing +Topic :: Games/Entertainment :: Side-Scrolling/Arcade Games +Topic :: Games/Entertainment :: Simulation +Topic :: Games/Entertainment :: Turn Based Strategy +Topic :: Home Automation +Topic :: Internet :: File Transfer Protocol (FTP) +Topic :: Internet :: Finger +Topic :: Internet :: Log Analysis +Topic :: Internet :: Name Service (DNS) +Topic :: Internet :: Proxy Servers +Topic :: Internet :: WAP +Topic :: Internet :: WWW/HTTP +Topic :: Internet :: WWW/HTTP :: Browsers +Topic :: Internet :: WWW/HTTP :: Dynamic Content +Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries +Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System +Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Message Boards +Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary +Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Page Counters +Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Wiki +Topic :: Internet :: WWW/HTTP :: HTTP Servers +Topic :: Internet :: WWW/HTTP :: Indexing/Search +Topic :: Internet :: WWW/HTTP :: Session +Topic :: Internet :: WWW/HTTP :: Site Management +Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking +Topic :: Internet :: WWW/HTTP :: WSGI +Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware +Topic :: Internet :: WWW/HTTP :: WSGI :: Server +Topic :: Internet :: XMPP +Topic :: Internet :: Z39.50 +Topic :: Multimedia +Topic :: Multimedia :: Graphics +Topic :: Multimedia :: Graphics :: 3D Modeling +Topic :: Multimedia :: Graphics :: 3D Rendering +Topic :: Multimedia :: Graphics :: Capture +Topic :: Multimedia :: Graphics :: Capture :: Digital Camera +Topic :: Multimedia :: Graphics :: Capture :: Scanners +Topic :: Multimedia :: Graphics :: Capture :: Screen Capture +Topic :: Multimedia :: Graphics :: Editors +Topic :: Multimedia :: Graphics :: Editors :: Raster-Based +Topic :: Multimedia :: Graphics :: Editors :: Vector-Based +Topic :: Multimedia :: Graphics :: Graphics Conversion +Topic :: Multimedia :: Graphics :: Presentation +Topic :: Multimedia :: Graphics :: Viewers +Topic :: Multimedia :: Sound/Audio +Topic :: Multimedia :: Sound/Audio :: Analysis +Topic :: Multimedia :: Sound/Audio :: CD Audio +Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Playing +Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping +Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Writing +Topic :: Multimedia :: Sound/Audio :: Capture/Recording +Topic :: Multimedia :: Sound/Audio :: Conversion +Topic :: Multimedia :: Sound/Audio :: Editors +Topic :: Multimedia :: Sound/Audio :: MIDI +Topic :: Multimedia :: Sound/Audio :: Mixers +Topic :: Multimedia :: Sound/Audio :: Players +Topic :: Multimedia :: Sound/Audio :: Players :: MP3 +Topic :: Multimedia :: Sound/Audio :: Sound Synthesis +Topic :: Multimedia :: Sound/Audio :: Speech +Topic :: Multimedia :: Video +Topic :: Multimedia :: Video :: Capture +Topic :: Multimedia :: Video :: Conversion +Topic :: Multimedia :: Video :: Display +Topic :: Multimedia :: Video :: Non-Linear Editor +Topic :: Office/Business +Topic :: Office/Business :: Financial +Topic :: Office/Business :: Financial :: Accounting +Topic :: Office/Business :: Financial :: Investment +Topic :: Office/Business :: Financial :: Point-Of-Sale +Topic :: Office/Business :: Financial :: Spreadsheet +Topic :: Office/Business :: Groupware +Topic :: Office/Business :: News/Diary +Topic :: Office/Business :: Office Suites +Topic :: Office/Business :: Scheduling +Topic :: Other/Nonlisted Topic +Topic :: Printing +Topic :: Religion +Topic :: Scientific/Engineering +Topic :: Scientific/Engineering :: Artificial Intelligence +Topic :: Scientific/Engineering :: Artificial Life +Topic :: Scientific/Engineering :: Astronomy +Topic :: Scientific/Engineering :: Atmospheric Science +Topic :: Scientific/Engineering :: Bio-Informatics +Topic :: Scientific/Engineering :: Chemistry +Topic :: Scientific/Engineering :: Electronic Design Automation (EDA) +Topic :: Scientific/Engineering :: GIS +Topic :: Scientific/Engineering :: Human Machine Interfaces +Topic :: Scientific/Engineering :: Hydrology +Topic :: Scientific/Engineering :: Image Processing +Topic :: Scientific/Engineering :: Image Recognition +Topic :: Scientific/Engineering :: Information Analysis +Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator +Topic :: Scientific/Engineering :: Mathematics +Topic :: Scientific/Engineering :: Medical Science Apps. +Topic :: Scientific/Engineering :: Physics +Topic :: Scientific/Engineering :: Visualization +Topic :: Security +Topic :: Security :: Cryptography +Topic :: Sociology +Topic :: Sociology :: Genealogy +Topic :: Sociology :: History +Topic :: Software Development +Topic :: Software Development :: Assemblers +Topic :: Software Development :: Bug Tracking +Topic :: Software Development :: Build Tools +Topic :: Software Development :: Code Generators +Topic :: Software Development :: Compilers +Topic :: Software Development :: Debuggers +Topic :: Software Development :: Disassemblers +Topic :: Software Development :: Documentation +Topic :: Software Development :: Embedded Systems +Topic :: Software Development :: Internationalization +Topic :: Software Development :: Interpreters +Topic :: Software Development :: Libraries +Topic :: Software Development :: Libraries :: Application Frameworks +Topic :: Software Development :: Libraries :: Java Libraries +Topic :: Software Development :: Libraries :: PHP Classes +Topic :: Software Development :: Libraries :: Perl Modules +Topic :: Software Development :: Libraries :: Pike Modules +Topic :: Software Development :: Libraries :: Python Modules +Topic :: Software Development :: Libraries :: Ruby Modules +Topic :: Software Development :: Libraries :: Tcl Extensions +Topic :: Software Development :: Libraries :: pygame +Topic :: Software Development :: Localization +Topic :: Software Development :: Object Brokering +Topic :: Software Development :: Object Brokering :: CORBA +Topic :: Software Development :: Pre-processors +Topic :: Software Development :: Quality Assurance +Topic :: Software Development :: Testing +Topic :: Software Development :: Testing :: Acceptance +Topic :: Software Development :: Testing :: BDD +Topic :: Software Development :: Testing :: Mocking +Topic :: Software Development :: Testing :: Traffic Generation +Topic :: Software Development :: Testing :: Unit +Topic :: Software Development :: User Interfaces +Topic :: Software Development :: Version Control +Topic :: Software Development :: Version Control :: Bazaar +Topic :: Software Development :: Version Control :: CVS +Topic :: Software Development :: Version Control :: Git +Topic :: Software Development :: Version Control :: Mercurial +Topic :: Software Development :: Version Control :: RCS +Topic :: Software Development :: Version Control :: SCCS +Topic :: Software Development :: Widget Sets +Topic :: System +Topic :: System :: Archiving +Topic :: System :: Archiving :: Backup +Topic :: System :: Archiving :: Compression +Topic :: System :: Archiving :: Mirroring +Topic :: System :: Archiving :: Packaging +Topic :: System :: Benchmark +Topic :: System :: Boot +Topic :: System :: Boot :: Init +Topic :: System :: Clustering +Topic :: System :: Console Fonts +Topic :: System :: Distributed Computing +Topic :: System :: Emulators +Topic :: System :: Filesystems +Topic :: System :: Hardware +Topic :: System :: Hardware :: Hardware Drivers +Topic :: System :: Hardware :: Mainframes +Topic :: System :: Hardware :: Symmetric Multi-processing +Topic :: System :: Installation/Setup +Topic :: System :: Logging +Topic :: System :: Monitoring +Topic :: System :: Networking +Topic :: System :: Networking :: Firewalls +Topic :: System :: Networking :: Monitoring +Topic :: System :: Networking :: Monitoring :: Hardware Watchdog +Topic :: System :: Networking :: Time Synchronization +Topic :: System :: Operating System +Topic :: System :: Operating System Kernels +Topic :: System :: Operating System Kernels :: BSD +Topic :: System :: Operating System Kernels :: GNU Hurd +Topic :: System :: Operating System Kernels :: Linux +Topic :: System :: Power (UPS) +Topic :: System :: Recovery Tools +Topic :: System :: Shells +Topic :: System :: Software Distribution +Topic :: System :: System Shells +Topic :: System :: Systems Administration +Topic :: System :: Systems Administration :: Authentication/Directory +Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP +Topic :: System :: Systems Administration :: Authentication/Directory :: NIS +Topic :: Terminals +Topic :: Terminals :: Serial +Topic :: Terminals :: Telnet +Topic :: Terminals :: Terminal Emulators/X Terminals +Topic :: Text Editors +Topic :: Text Editors :: Emacs +Topic :: Text Editors :: Integrated Development Environments (IDE) +Topic :: Text Editors :: Text Processing +Topic :: Text Editors :: Word Processors +Topic :: Text Processing +Topic :: Text Processing :: Filters +Topic :: Text Processing :: Fonts +Topic :: Text Processing :: General +Topic :: Text Processing :: Indexing +Topic :: Text Processing :: Linguistic +Topic :: Text Processing :: Markup +Topic :: Text Processing :: Markup :: HTML +Topic :: Text Processing :: Markup :: LaTeX +Topic :: Text Processing :: Markup :: Markdown +Topic :: Text Processing :: Markup :: SGML +Topic :: Text Processing :: Markup :: VRML +Topic :: Text Processing :: Markup :: XML +Topic :: Text Processing :: Markup :: reStructuredText +Topic :: Utilities""".split("\n") + +try: + from trove_classifiers import classifiers + topic_trove = [line for line in classifiers if re.match("Topic ::", line)] +except: + pass ADDED pytest.ini Index: pytest.ini ================================================================== --- pytest.ini +++ pytest.ini @@ -0,0 +1,7 @@ +[pytest] +minversion = 1.0 +addopts = -vv -ra -q --ignore=test/_*.py --ignore=test/__*.py -p no:warnings +testpaths = test/ +python_files = *.py +# Yes sure pytest, why not do the obvious thing anyway +python_functions = !_* [a-z]* [A-Z]* !_* ADDED setup.cfg Index: setup.cfg ================================================================== --- setup.cfg +++ setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 ADDED setup.py Index: setup.py ================================================================== --- setup.py +++ setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# api: pip +# type: build +# title: config for setuptools +# +# Always prefer setuptools over distutils +# + +import sys +sys.path.insert(0, ".") # bootstrap local ./pluginconf module + +import pluginconf.setup + +#from os import system +#system("pandoc -f markdown -t rst README.md > README.rst") + + +pluginconf.setup.setup( + fn="pluginconf/__init__.py", + long_description="README.md" + #author_email="m..@include-once.org", +) + ADDED test/.pyz.d/__init__.py Index: test/.pyz.d/__init__.py ================================================================== --- test/.pyz.d/__init__.py +++ test/.pyz.d/__init__.py @@ -0,0 +1,6 @@ +# type: init + +""" set up loading mechanism? """ + +import os.path +__path__ = [os.path.dirname(__file__)] ADDED test/.pyz.d/__main__.py Index: test/.pyz.d/__main__.py ================================================================== --- test/.pyz.d/__main__.py +++ test/.pyz.d/__main__.py ADDED test/.pyz.d/config.py Index: test/.pyz.d/config.py ================================================================== --- test/.pyz.d/config.py +++ test/.pyz.d/config.py ADDED test/.pyz.d/inner.py Index: test/.pyz.d/inner.py ================================================================== --- test/.pyz.d/inner.py +++ test/.pyz.d/inner.py @@ -0,0 +1,15 @@ +# type: pyz +# category: complex +# title: pyz module +# desription: lookup within zipped packages/apps +# config: +# { name: relation, type: bool, val: 1, description: none } +# state: alpha +# +# doc here + + +import os.path +__path__ = [os.path.dirname(__file__)] + +print("inner module invoked") ADDED test/.pyz.pyz Index: test/.pyz.pyz ================================================================== --- test/.pyz.pyz +++ test/.pyz.pyz cannot compute difference between binary files ADDED test/Makefile Index: test/Makefile ================================================================== --- test/Makefile +++ test/Makefile @@ -0,0 +1,4 @@ +pyz: .pyz.pyz + +.pyz.pyz: .pyz.d/* + cd .pyz.d ; zip ../.pyz.pyz * ADDED test/__init__.py Index: test/__init__.py ================================================================== --- test/__init__.py +++ test/__init__.py @@ -0,0 +1,1 @@ +""" stub, to recognize test/ as module= """ ADDED test/basic.py Index: test/basic.py ================================================================== --- test/basic.py +++ test/basic.py @@ -0,0 +1,25 @@ +# type: test +# title: basic PMD +# description: check for some fields +# version: 0.1-rc1 +# +# This the doc. + +import pytest +import pluginconf + +@pytest.fixture +def pmd(): + return pluginconf.plugin_meta(fn=__file__) + +def type_(pmd): + assert pmd["type"] == "test" + +def version_(pmd): + assert pmd["version"] == "0.1-rc1" + +def title_(pmd): + assert pmd["title"] == "basic PMD" + +def doc_(pmd): + assert pmd["doc"] == "This the doc." ADDED test/config.py Index: test/config.py ================================================================== --- test/config.py +++ test/config.py @@ -0,0 +1,36 @@ +# type: test +# title: config struct +# description: probe for some fields +# config: +# { name: id, value: 1, description: identifier } +# { name: string, value: string, description: should be a str } +# { name: desc, value: -, description: be descriptive here } +# { name: select1, type: select, select: 1|2|3, description: alternatives } +# { name: select2, type: select, select: a=1|b=2|c=3, description: key-val } +# version: 0.1 +# +# Do all the settings! + +import pytest +import pluginconf + +@pytest.fixture +def config(): + return pluginconf.plugin_meta(fn=__file__)["config"] + +def name(config): + assert config[0]["name"] == "id" + +def string(config): + assert config[1]["value"] == "string" + assert config[1]["type"] == "str" + +def desc(config): + assert config[2]["description"] == "be descriptive here" + +def select1(config): + assert config[3]["select"] == { "1":"1", "2":"2", "3":"3" } + +def select2(config): + assert config[4]["select"] == { "a":"1", "b":"2", "c":"3" } + ADDED test/config_complex.py Index: test/config_complex.py ================================================================== --- test/config_complex.py +++ test/config_complex.py @@ -0,0 +1,20 @@ +# type: test +# title: config edge cases +# description: some less stable options +# config: +# { name: nested, value: "{var}", description: "should be able to understand {enclosed} braces" } +# version: 0.1 +# +# Do all the settings! + +import pytest +import pluginconf + +@pytest.fixture +def config(): + return pluginconf.plugin_meta(fn=__file__)["config"] + +def name(config): + print(config) + assert config[0]["value"] == "{var}" + ADDED test/conftest.py Index: test/conftest.py ================================================================== --- test/conftest.py +++ test/conftest.py @@ -0,0 +1,2 @@ +import pytest +import pluginconf as pc ADDED test/pyz.py Index: test/pyz.py ================================================================== --- test/pyz.py +++ test/pyz.py @@ -0,0 +1,37 @@ +# type: test +# version: 0.2 +# title: pyz lookups +# +# This is genuinely the most fragile part. +# Not the least testing it from outside the pyz package. +# Here `app.pyz/inner.py` must override its lookup __path__=[…] +# Fails from within test/xyz/ subdir + +import sys +import os.path +import pytest +import pluginconf + +sys.path.insert(0, f"{os.path.dirname(__file__)}/.pyz.pyz") +os.chdir(os.path.dirname(__file__)) + +pluginconf.module_base = ["config"] # must be one of the .pyz-contained modules (should be a dir/submodule for real uses) +pluginconf.plugin_base = ["inner"] # relative, must declare __path__, can't be __main__.py + +@pytest.fixture +def pmd(): + return pluginconf.plugin_meta(module="inner") + +def importy(): + import inner as pyz_inner + #print(pyz_inner.__file__) + +def module_list(): + assert set(pluginconf.module_list()) & {'__main__', 'config', 'inner'} + +def inner_props(pmd): + assert pmd["type"] == "pyz" + assert pmd["category"] == "complex" + assert pmd["title"] == "pyz module" + assert pmd["config"][0]["name"] == "relation" + assert pmd["state"] == "alpha" ADDED test/sources.py Index: test/sources.py ================================================================== --- test/sources.py +++ test/sources.py @@ -0,0 +1,32 @@ +# type: test +# title: frame= +# description: test alternative source aqusition +# ok: 1 + +import pytest +import pluginconf +import sys + +def _pmd(**kwargs): + return pluginconf.plugin_meta(**kwargs) + +def frame_1(): + assert _pmd(frame=1)["ok"] + +def frame_0(): + # should resolve to pluginconf/__init__.py + assert _pmd(frame=0)["title"] == "Plugin configuration" + +def src(): + # requires trailing newline + assert _pmd(src="# id: empty\n")["id"] == "empty" + +def module_pc_init(): + pluginconf.plugin_base = ["pluginconf", "."] + assert _pmd(module="__init__")["category"] == "config" + +def module_test(): + # requires __init__.py in test/ + pluginconf.plugin_base = ["test"] + assert _pmd(module="sources")["ok"] +