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.
-
-
+Plugin meta extraction and module lookup.
+
+
"""
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: