Index: pluginconf/__init__.py ================================================================== --- pluginconf/__init__.py +++ pluginconf/__init__.py @@ -2,11 +2,11 @@ # api: python ##type: extract # category: config # title: Plugin configuration # description: Read meta data, pyz/package contents, module locating -# version: 0.7.9 +# version: 0.8.0 # state: stable # classifiers: documentation # depends: python >= 2.7 # suggests: python:flit, python:PySimpleGUI # license: PD @@ -21,10 +21,12 @@ # console-scripts: flit-pluginconf=pluginconf.flit:main, pluginconf.flit=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. @@ -39,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 @@ -65,39 +70,47 @@ # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # 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(). -# -# Ideally a designated reference base_module should be extended with -# new lookup locations via `module.__path__ += ["./local"]` for exmple. -# Plugin loading thus becomes as simple as __import__("ext.local"). -# -# Using 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 project.) +# 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(filename=…)` unpacks - [meta fields](https://fossil.include-once.org/pluginspec/) - into dictionaries. - * Other utility code is about module location, but requires - some initialization. - -![#](https://fossil.include-once.org/pluginspec/logo) +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. +
  • //pypi.org/project/pluginconf/ +
  • //fossil.include-once.org/pluginspec/ +
  • """ import sys import os +import os.path import re import functools import pkgutil import inspect try: @@ -111,36 +124,37 @@ """ haphazard workaround """ return zlib.decompress(bytestr, 16 + zlib.MAX_WBITS) import zipfile import argparse import logging +#logging.basicConfig(level=logging.DEBUG) __all__ = [ "plugin_meta", "get_data", "module_list", "add_plugin_defaults", - "PluginMeta", "all_plugin_meta", + "PluginMeta", "OptionList", "all_plugin_meta", + "data_root", "plugin_base", "config_opt_type_map", ] # Injectables # ‾‾‾‾‾‾‾‾‾‾‾ -""" injectable callback function for logging """ log = logging.getLogger("pluginconf") +""" injectable callback function for logging """ +data_root = "config" # inspect.getmodule(sys._getframe(1)).__name__ """ 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"] - -""" normalize config type: names to `str`, `text`, `bool`, `int`, `select`, `dict` """ +(Equivalent to `PluginBase(package=…)`) +""" + +plugin_base = ["plugins"] +""" +Package/module names (or directories) for module_list() and plugin_meta() +lookups. Associated paths (`__path__`) will be scanned for module/plugin +basenames. (Similar to `PluginBase(searchpath=…)`) +""" + config_opt_type_map = { "longstr": "text", "string": "str", "boolean": "bool", "checkbox": "bool", @@ -149,10 +163,11 @@ "choice": "select", "options": "select", "table": "dict", "array": "dict" } +""" normalize config type: names to `str`, `text`, `bool`, `int`, `select`, `dict` """ # Compatiblity # ‾‾‾‾‾‾‾‾‾‾‾‾ def renamed_arguments(renamed): @@ -169,41 +184,41 @@ # Resource retrieval # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @renamed_arguments({"fn": "filename", "gz": "gzip"}) -def get_data(filename, decode=False, gzip=False, file_base=None): +def get_data(filename, decode=False, gzip=False, file_root=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. + Utilizes the data_root as top-level reference. Parameters ---------- filename : str filename in pyz or bundle decode : bool text file decoding utf-8 gzip : bool automatic gzdecode - file_base : list - alternative base module reference + file_root : list + alternative base module (application or pyz root) Returns ------- str : file contents """ try: - data = pkgutil.get_data(file_base or module_base, filename) + data = pkgutil.get_data(file_root or data_root, filename) if gzip: data = gzip_decode(data) if decode: return data.decode("utf-8", errors='ignore') return str(data) - except: - log.error("get_data() didn't find:", fn, "in", file_base) + except: #(FileNotFoundError, IOError, OSError, ImportError, gzip.BadGzipFile): + log.error("get_data() didn't find '%s' in '%s'", filename, file_root) pass # Plugin name lookup # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @@ -224,11 +239,16 @@ # Convert plugin_base package names into paths for iter_modules paths = [] for module_or_path in plugin_base: if sys.modules.get(module_or_path): - paths += sys.modules[module_or_path].__path__ + try: + paths += sys.modules[module_or_path].__path__ + except AttributeError: + paths += os.path.dirname(os.path.realname( + sys.modules[module_or_path] + )) elif os.path.exists(module_or_path): paths.append(module_or_path) # Should list plugins within zips as well as local paths dirs = pkgutil.iter_modules(paths + (extra_paths or [])) @@ -255,11 +275,11 @@ # Plugin meta data extraction # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @renamed_arguments({"fn": "filename"}) def plugin_meta(filename=None, src=None, module=None, frame=1, **kwargs): """ - Extract plugin meta data block from different sources: + Extract plugin meta data block from specified source. Parameters ---------- filename : str Read literal files, or .pyz contents. @@ -270,27 +290,32 @@ frame : int Extract comment header of caller (default). extra_base : list Additional search directories. max_length : list - Maximum size to read from files. + Maximum size to read from files (6K default). Returns ------- dict : Extracted comment fields, with config: preparsed + + The result dictionary has fields accessible as e.g. `pmd["title"]` + or `pmd.version`. The documentation block after all fields: is called + ["doc"]`. And `pmd.config` already parsed as a list of dictionaries. """ # Try via pkgutil first, # find any plugins.* modules, or main packages if module: filename = module for base in plugin_base + kwargs.get("extra_base", []): try: - src = get_data(filename=filename+".py", decode=True, file_base=base) + #log.debug(f"mod={base} fn={filename}.py") + src = get_data(filename=filename+".py", decode=True, file_root=base) if src: break - except: + except (IOError, OSError, FileNotFoundError): continue # plugin_meta_extract() will print a notice later # Real filename/path elif filename and os.path.exists(filename): src = open(filename).read(kwargs.get("max_length", 6144)) @@ -311,11 +336,11 @@ src = zipfile.ZipFile(filename, "r").read(int_fn.strip("/")) # Extract source comment into meta dict if not src: src = "" - if not isinstance(src, str): + if isinstance(src, bytes): src = src.decode("utf-8", errors='replace') return plugin_meta_extract(src, filename) # Comment and field extraction logic @@ -355,11 +380,11 @@ # Extract coherent comment block src = src.replace("\r", "") if not literal: src = rx.comment.search(src) if not src: - log.error("Couldn't read source meta information:", filename) + log.warn("Couldn't read source meta information: %s", filename) return meta src = src.group(0) src = rx.hash.sub("", src).strip() # Split comment block @@ -376,13 +401,13 @@ # Dict/list wrappers # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ class PluginMeta(dict): """ - Plugin meta data, as dictionary with alternative .property access. - Returned for each `plugin_meta()` result, and config: options. - Non-existent .fieldnames just resolve to `""`. + Plugin meta data as dictionary`{}`, or alternatively `.property` access. + Returned for each `plugin_meta()` result, and individual `config:` options. + Absent `.field` access resolves to `""`. """ def __getattr__(self, key, default=""): """ Return [key] for .property access, else `""`. """ if key == "config": @@ -393,11 +418,11 @@ """ Shouldn't really have this, but for parity. """ self[key] = val class OptionList(list): """ - List of config: options, with additional .name access (name= from option entry). + List of `config:` options, with alernative `.name` access (lookup by name= from option entry). """ def __getattr__(self, key): """ Returns list entry with name= equaling .name access """ for opt in self: