Index: README.md ================================================================== --- README.md +++ README.md @@ -9,11 +9,11 @@ # 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 + # docs: https://fossil.include-once.org/pluginspec/ # config: # { name: xyz, value: 1, type: bool, description: "Sets..." } # { name: var, value: "init", description: "Another..." } # license: MITL # @@ -26,11 +26,11 @@ for options and defaults. How it's used: import pluginconf - meta = pluginconf.plugin_meta(fn="./plugin/func.py") + meta = pluginconf.plugin_meta(filename="./plugin/func.py") print(meta) What it’s not: > * This is not another config reader/parser/storage class. @@ -41,11 +41,11 @@ > * 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. +pluginconf is foremost about the universal meta comment format. # API Lookup configuration is currently just done through injection: @@ -56,11 +56,11 @@ 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= ) +#### plugin_meta( module= | filename= | src= | frame= ) Returns a meta data dictionary for the given module name, file, source code, or caller frame: { "title": "Compound★", @@ -95,15 +95,15 @@ conf = { "defaults": "123", "plugins": {} # ← stores the activation states } - for module,meta in pluginconf.all_plugin_meta().items(): + 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= ) +#### get_data( filename= ) Is mostly an alias for pkgutil.get_data(). Abstracts usage within PYZ packages a little. #### argparse_map() @@ -156,11 +156,11 @@ 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" + filename="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` @@ -167,10 +167,16 @@ 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. + +# other modules + + * DependencyValidation is now in `pluginconf.depends` + * `pluginconf.flit` is meant as alternative to setup. + #### Caveats * It’s mostly just an excerpt from streamtuner2. * Might need customization prior use. @@ -184,6 +190,7 @@ * 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. + Index: pluginconf/__init__.py ================================================================== --- pluginconf/__init__.py +++ pluginconf/__init__.py @@ -10,10 +10,12 @@ # license: PD # priority: core # docs: https://fossil.include-once.org/pluginspec/ # url: http://fossil.include-once.org/streamtuner2/wiki/plugin+meta+data # config: - +# format: off +# pylint: disable=invalid-name # # 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. # @@ -72,10 +74,12 @@ # # 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. +""" Plugin meta extraction and module lookup""" + import sys import os import re import functools @@ -114,10 +118,24 @@ 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"] + +""" normalize config type: names to `str`, `text`, `bool`, `int`, `select`, `dict` """ +config_opt_type_map = { + "longstr": "text", + "string": "str", + "boolean": "bool", + "checkbox": "bool", + "integer": "int", + "number": "int", + "choice": "select", + "options": "select", + "table": "dict", + "array": "dict" +} # Compatiblity # ‾‾‾‾‾‾‾‾‾‾‾‾ def renamed_arguments(renamed): @@ -170,19 +188,19 @@ :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): - paths += sys.modules[mp].__path__ - elif os.path.exists(mp): - paths.append(mp) + for module_or_path in plugin_base: + if sys.modules.get(module_or_path): + paths += sys.modules[module_or_path].__path__ + elif os.path.exists(module_or_path): + paths.append(module_or_path) # Should list plugins within zips as well as local paths - ls = pkgutil.iter_modules(paths + (extra_paths or [])) - return [name for loader, name, ispkg in ls] + dirs = pkgutil.iter_modules(paths + (extra_paths or [])) + return [name for loader, name, ispkg in dirs] # Plugin => meta dict # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ def all_plugin_meta(): @@ -196,16 +214,16 @@ } # Plugin meta data extraction # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -@renamed_arguments({"filename": "fn"}) -def plugin_meta(fn=None, src=None, module=None, frame=1, **kwargs): +@renamed_arguments({"fn": "filename"}) +def plugin_meta(filename=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 filename: 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 @@ -212,69 +230,70 @@ """ # Try via pkgutil first, # find any plugins.* modules, or main packages if module: - fn = module + filename = module for base in plugin_base + kwargs.get("extra_base", []): try: - src = get_data(fn=fn+".py", decode=True, file_base=base) + src = get_data(filename=filename+".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(kwargs.get("max_length", 6144)) + elif filename and os.path.exists(filename): + src = open(filename).read(kwargs.get("max_length", 6144)) # Else get source directly from caller - elif not src and not fn: + elif not src and not filename: module = inspect.getmodule(sys._getframe(frame+1)) # decorator+1 - fn = inspect.getsourcefile(module) + filename = 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("/")) + # Assume it's a filename matches …/base.zip/…/int.py + elif filename: + int_fn = "" + while len(filename) and not os.path.exists(filename): # pylint: disable=len-as-condition + filename, add = os.path.split(filename) + int_fn = add + "/" + int_fn + if len(filename) >= 3 and int_fn and zipfile.is_zipfile(filename): + src = zipfile.ZipFile(filename, "r").read(int_fn.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) + return plugin_meta_extract(src, filename) # Comment and field extraction logic # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -def plugin_meta_extract(src="", fn=None, literal=False): +@renamed_arguments({"fn": "filename"}) +def plugin_meta_extract(src="", filename=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 + :arg str src: from existing source code + :arg int filename: 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, + "id": os.path.splitext(os.path.basename(filename or ""))[0], + "fn": filename, "api": "python", "type": "module", "category": None, "priority": None, "version": "0", - "title": fn, + "title": filename, "description": "no description", "config": [], "doc": "" } @@ -281,11 +300,11 @@ # 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) + log_ERR("Couldn't read source meta information:", filename) return meta src = src.group(0) src = rx.hash.sub("", src).strip() # Split comment block @@ -300,25 +319,25 @@ return meta # Unpack config: structures # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -def plugin_meta_config(str): +def plugin_meta_config(src): """ Further breaks up the meta['config'] descriptor. Creates an array from JSON/YAML option lists. - :arg str str: unprocessed config: field + :arg str src: 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): + for entry in rx.config.findall(src): entry = entry[0] or entry[1] opt = { "type": None, "name": None, "description": "", @@ -333,21 +352,16 @@ 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" -) +def config_opt_parse_select(select): + """ unpack 1|2|3 or title=lists """ + if re.search("([=:])", select): + return dict(rx.select_dict.findall(select)) + #else: + return {val: val for val in rx.select_list.findall(select)} # Comment extraction regexps # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ class rx: