Files in the top-level directory of check-in 63fdedee18b22b1c


Provides meta data extraction and plugin basename lookup. And it’s meant for in-application feature and option management. The descriptor format (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: https://fossil.include-once.org/pluginspec/
# 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 for options and defaults.

How it's used:

import pluginconf
meta = pluginconf.plugin_meta(filename="./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 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.

API

Lookup configuration is currently just done through injection:

plugin_base = [__package__, "myapp.plugins", "/usr/share/app/extensions"]
data_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= | filename= | 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.data_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( filename= )

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 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(
     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 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 is an even more versatile wrapper with sensible defaults and source scanning.

flit wrapper

Alternatively, there's pluginconf.flit to utilize pyproject.toml for building packages, while sourcing meta data from the primary package file.

 [build-system]
 requires = ["pluginconf", "flit"]
 build-backend = "pluginconf.flit"

 [project]
 name = "projectname"

It can be invoked via flit-pluginconf build / python -m pluginconf.flit build or even python -m build. Field mapping isn't very robust yet, and mercilessly flaunts the dynamic= directive.

other modules

  • pluginconf.depends provides Check for .valid() and .depends() probing
  • pluginconf.bind is a simpler interface and basic plugin loader
  • argparse_map() might also end up in a separate module.

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 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 likeclass: 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.