File README.md artifact 22794d9a4b part of check-in 77cb85f450
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 perpluginconf.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 leavefiles=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
providesCheck
for .valid() and .depends() probingpluginconf.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:
orslot:
. (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.