Index: html/depends.html ================================================================== --- html/depends.html +++ html/depends.html @@ -48,28 +48,63 @@

The .valid() helper only asserts the api: string, or skips existing modules, and if they're more recent. While .depends() compares minimum versions against existing modules.

In practice there's little need for full-blown dependency resolving for application-level modules.

-

Attributes

-
-
api : list
-
allowed api: identifiers for .valid() stream checks
-
log : logging
-
warning handler
-
have : dict
-
accumulated list of existing/virtual plugins
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
apilistallowed api: identifiers for .valid() stream checks
system_depsboolcheck bin:app or python:package dependencies
logloggingwarning handler
havedictaccumulated list of existing/virtual plugins

Prepare list of known plugins and versions in self.have={}

-

Parameters

-
-
add : dict
-
name to pmd list of existing/core/virtual plugins (can define -versions or own dependencies)
-
core : list
-
name list of virtual plugins
-
+ + + + + + + + + + + + + + + + + + + + +
Parameters
adddictname→pmd of existing/core plugins (incl ver or deps)
corelistname list of virtual plugins

Class variables

var api
@@ -99,11 +134,32 @@
def depends(self, plugin)
-

Verify depends: and breaks: against existing plugins/modules

+

Verify depends: and breaks: against existing plugins/modules

+ + + + + + + + + + + + + + + + + + + + +
Parameters
plugindictplugin meta properties of (new?) plugin
Returnsboolmatches up with existing .have{} installation
def module_test(self, urn, name)
@@ -127,11 +183,32 @@ def valid(self, new_plugin)

Plugin pre-screening from online repository stream. Fields are $name, $file, $dist, api, id, depends, etc -Exclude installed or for newer-version presence.

+Exclude installed or for newer-version presence.

+ + + + + + + + + + + + + + + + + + + + +
Parameters
new_plugindictonline properties of available plugin
Returnsboolis updatatable
Index: html/flit.html ================================================================== --- html/flit.html +++ html/flit.html @@ -28,11 +28,11 @@
pyproject.toml foobar/__init__.py
 [build-system]
-requires = ["pluginconf", "flit]
+requires = ["pluginconf", "flit"]
 build-backend = "pluginconf.flit"
 
 [project]
 name = "foobar"
 dynamic = ["*"]

Index: html/gui.html
==================================================================
--- html/gui.html
+++ html/gui.html
@@ -46,11 +46,11 @@
 
 
def plugin_layout(pmd_list, config, plugin_states, opt_label=False)
-

craft list of widgets for each read plugin

+

craft list of widgets: *( plugin_entry(), *option_entry() )

def read_options(files)
@@ -63,32 +63,61 @@

Reads *.py files and crafts a settings dialog from meta data.

Where plugin_states{} is usually an entry in config{} itself. Depending on plugin and option names, it might even be a flat/shared namespace for both. Per default you'd set files=["plugins/*.py", __file__] to be read. But with files=[] it's possible to provide a plugins=pluginconf.get_plugin_meta() or prepared plugin/options dict instead.

-

Parameters

-
-
config : dict 🔁
-
Config settings, updated after dialog completion
-
plugin_states : dict 🔁
-
Plugin activation states, also input/output
-
files : list
-
Glob list of *.py files to extract meta definitions from
-
plugins : dict
-
Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected
-
opt_label : bool
-
Show config name= as label
-
theme : str
-
Set PSG window theme.
-
**kwargs : dict
-
Other options are passed on to PySimpleGUI
-
-

Returns

-
-
True : if changed config{} values are to be saved (the dict will be updated in any case)
-
 
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Params
configdict 🔁Config settings, updated after dialog completion
plugin_statesdict 🔁Plugin activation states, also input/output
fileslistGlob list of *.py files to extract meta definitions from
pluginsdictAlternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected
opt_labelboolShow config name= as label (instead of description)
themestrSet PSG window theme.
**kwargsdictOther options are passed on to PySimpleGUI
ReturnsTrueif updated config{} values should be [Saved]
def wrap(text, width=50)
Index: html/index.html ================================================================== --- html/index.html +++ html/index.html @@ -88,13 +88,12 @@
def add_plugin_defaults(conf_options, conf_plugins, meta, module='')
-

Utility function which collect defaults from plugin meta data to -a config store. Which in the case of streamtuner2 is really just a -dictionary conf{} and a plugin list in conf.plugins{}.

+

Utility function to collect defaults from plugin meta data to +a config dict/store.

@@ -103,16 +102,16 @@ - + - + @@ -147,11 +146,11 @@ - +
Parameters
conf_options dict 🔁storage for amassed optionsstorage for amassed #config: options
conf_plugins dict 🔁enable status based on plugin state/priority:activation status derived from state/priority:
meta dict input plugin meta data (invoke once per plugin)
Returns dictnames to meta data dictnames to PluginMeta dict
Index: html/setup.html ================================================================== --- html/setup.html +++ html/setup.html @@ -47,19 +47,36 @@ def setup(filename=None, debug=False, **kwargs)

Wrapper around setuptools.setup() which adds some defaults and plugin meta data import, with some shortcut parameters:

-

Parameters

-
-
filename : str
-
main file "pkg/main.py" (else deduced from primary package name)
-
debug : bool
-
display collected options prior setuptools.setup() invocation
-
long_description : str
-
e.g. "README.md", else comment block used
-
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Parameters
filenamestrmain file "pkg/main.py" (else deduced from primary package name)
debugbooldisplay collected options prior setuptools.setup() invocation
long_descriptionstre.g. "README.md", else the comment block gets used

Other setup() params work as usual, and are passed trough. Notably entry_points= or data_files= can be used, even if they get augmented.

Index: pluginconf/__init__.py ================================================================== --- pluginconf/__init__.py +++ pluginconf/__init__.py @@ -11,11 +11,11 @@ # suggests: python:flit, python:PySimpleGUI # license: PD # priority: core # api-docs: https://fossil.include-once.org/pluginspec/doc/trunk/html/index.html # docs: https://fossil.include-once.org/pluginspec/ -# url: http://fossil.include-once.org/streamtuner2/wiki/plugin+meta+data +# url: https://fossil.include-once.org/pluginspec/wiki/pluginconf # config: - # format: off # permissive: 0.75 # pylint: disable=invalid-name # console-scripts: flit-pluginconf=pluginconf.flit:main @@ -252,11 +252,11 @@ of available/installed plugins. It associates each plugin name with a its meta{} fields. | Parameters | | | |-------------|---------|---------------------------------| - | **Returns** | dict | names to meta data dict | + | **Returns** | dict | names to `PluginMeta` dict | """ return { name: plugin_meta(module=name) for name in module_list() } @@ -557,18 +557,17 @@ # Add plugin defaults to conf.* store # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ def add_plugin_defaults(conf_options, conf_plugins, meta, module=""): """ - Utility function which collect defaults from plugin meta data to - a config store. Which in the case of streamtuner2 is really just a - dictionary `conf{}` and a plugin list in `conf.plugins{}`. + Utility function to collect defaults from plugin meta data to + a config dict/store. | Parameters | | | |-------------|---------|--------------------------------------| - | conf_options| dict 🔁 | storage for amassed options | - | conf_plugins| dict 🔁 | enable status based on plugin state/priority: | + | conf_options| dict 🔁 | storage for amassed #config: options | + | conf_plugins| dict 🔁 | activation status derived from state/priority: | | meta | dict | input plugin meta data (invoke once per plugin)| | module | str | basename of meta: blocks plugin file | | **Returns** | None | - | """ Index: pluginconf/bind.py ================================================================== --- pluginconf/bind.py +++ pluginconf/bind.py @@ -7,18 +7,18 @@ # state: alpha # priority: optional # # Most basic plugin management/loader. It unifies meta data retrieval # and instantiation. It still doesn't dictate a plugin API or config -# storage (using json in examples). And can be a simple setup: +# storage (using json in examples). And can be a simpler setup: # # Create an empty plugins/__init__.py to use as package and for # plugin discovery. # # Designate it as such: # -# import pluginconf.bind +# import pluginconf.bind # (first import resets .plugin_base) # pluginconf.bind.base("plugins") # # Set up a default conf={} in your application, with some presets, # or updating it from a stored config file: # @@ -39,31 +39,46 @@ # Instantiate plugin modules based on load conf[plugins] state: # # for module in pluginconf.bind.load_enabled(conf): # module.register(main_window) # -# Using a simple init function often suffices, if plugins don't +# Electing a simple init function often suffices, if plugins don't # register themselves. Alternatively use a class name aligned with # the plugin basename to disover it, or dir(), or similar such. +# (This is what "pluginconf imposes no API" means: you still have +# to decide on a convention.) # # If you want users to update settings or plugin states, use the # .window module: # # pluginconf.gui.window(conf, conf["plugins"], files=["plugins/*.py"]) # json.dump(conf, open("app.json", "w")) # -# Alternatively there's the load() for known plugin names, or find() +# Alternatively there's load() for well-known plugin names, or find() # to uncover them based on descriptors. Or isolated() to instantiate # from a different set. # -# Notably the whole setup makes most sense if you have user-supplied -# plugin and some repository to fetch/update new ones from. (Out of -# scope here, but a zip/pyz download and extract might suffice). If -# so, entangle the alternative directory or pyz to be scanned: +# Notably the whole effort makes most sense if you have user-supplied +# plugins and some repository to fetch/update new ones from. (Optional +# meta descriptions are quite suitable to signify beta or experimental +# extensions). If so, entangle the alternative directory to be scanned: # # pluginconf.bind.base("plugins", dir="~/.config/app/usrplugs/") # pluginconf.bind.defaults(conf) # update +# +# A simpler user plugin mechanism might just download a zip: +# +# usr_pyz = f"{os.environ['HOME']}/.config/app/community.pyz" +# with open(usr_pyz, "w") as write: +# write.write( +# requests.get("http://example.org/usr-plugins.zip").content +# ) +# +# And register that as pyz instead (on startup): +# +# if os.path.exists(usr_pyz): +# pluginconf.bind.base("plugins", dir=usr_pyz) # # With PySimpleGUI, `conf` could be a psg.UserSettings("app.json") for # automatic settings+update storage, btw. # @@ -95,11 +110,11 @@ ### Find by type for name, pmd in pluginconf.bind.find(type="effect").items(): mod = pluginconf.bind.load(name) if pmd.category == "menu": - main_win.add_menu(mod.menu) + main_win.add_menu(mod.menu) Note that this uses meta data extraction, so should best be confined for app setup/initialization, not used recurringly. The config state usage is the preferred method. (Only requires property loading once, for installation or with `pluginconf.gui.window()` usage.) @@ -109,10 +124,11 @@ """ import os import sys import importlib +import functools import pluginconf #-- reset pluginconf if .bind is used pluginconf.plugin_base = [] @@ -150,11 +166,11 @@ | path | str | Bind directory or pyz/zip bundle to plugin_base. | | **Returns** | None | - | Module should be a package, as in a directory and init `plugins/__init__.py`. Ideally this module was already imported in main. But parameter may be a string. - + This could be invoked multiple times for the package name to append further path= arguments (=./contrib/, =/usr/share/app/extenstions/, or a .pyz). Which is functionally identical to delcaring `__path__` in the `package/__init__.py`. """ @@ -278,8 +294,22 @@ """ *return* defaults for isolated plugin structure 🧩 🧾 """ conf = {"plugins": {}} defaults(conf) return conf + +def _enable_cache(state=True): + """ + Reduce plugin_meta() lookup costs, suitable for repeat find() calls + """ + if hasattr(pluginconf.plugin_meta, "__wrapped__"): + if state: + return + pluginconf.plugin_meta = pluginconf.plugin_meta.__wrapped__ + elif state: + decorator = functools.lru_cache(maxsize=None) + pluginconf.plugin_meta = decorator(pluginconf.plugin_meta) + def _dirname(file): + """ absolute dirname for file """ return os.path.dirname(os.path.realpath(file)) Index: pluginconf/depends.py ================================================================== --- pluginconf/depends.py +++ pluginconf/depends.py @@ -58,18 +58,16 @@ While .depends() compares minimum versions against existing modules. In practice there's little need for full-blown dependency resolving for application-level modules. - Attributes - ---------- - api : list - allowed api: identifiers for .valid() stream checks - log : logging - warning handler - have : dict - accumulated list of existing/virtual plugins + | Attributes | | | + |------------|---------|-----------------------------------------------------| + | api | list | allowed api: identifiers for .valid() stream checks | + | system_deps| bool | check `bin:app` or `python:package` dependencies | + | log | logging | warning handler | + | have | dict | accumulated list of existing/virtual plugins | """ # supported APIs api = ["python", "streamtuner2"] @@ -81,17 +79,14 @@ def __init__(self, add=None, core=["st2", "uikit", "config", "action"]): """ Prepare list of known plugins and versions in self.have={} - Parameters - ---------- - add : dict - name to pmd list of existing/core/virtual plugins (can define - versions or own dependencies) - core : list - name list of virtual plugins + | Parameters | | | + |------------|---------|------------------------------------------------------| + | add | dict | name→pmd of existing/core plugins (incl ver or deps) | + | core | list | name list of virtual plugins | """ self.have = { "python": {"version": sys.version} } @@ -121,10 +116,15 @@ def valid(self, new_plugin): """ Plugin pre-screening from online repository stream. Fields are $name, $file, $dist, api, id, depends, etc Exclude installed or for newer-version presence. + + | Parameters | | | + |-------------|---------|------------------------------------------------------| + | new_plugin | dict | online properties of available plugin | + | **Returns** | bool | is updatatable | """ if not "$name" in new_plugin: self.log.warning(".valid() checks online plugin lists, requires $name") name = new_plugin.get("$name", "__invalid") have_ver = self.have.get(name, {}).get("version", "0") @@ -139,11 +139,18 @@ else: return True return False def depends(self, plugin): - """ Verify depends: and breaks: against existing plugins/modules """ + """ + Verify depends: and breaks: against existing plugins/modules + + | Parameters | | | + |-------------|---------|------------------------------------------------------| + | plugin | dict | plugin meta properties of (new?) plugin | + | **Returns** | bool | matches up with existing .have{} installation | + """ result = True if plugin.get("depends"): result &= self.and_or(self.split(plugin["depends"]), self.have) if plugin.get("breaks"): result &= self.neither(self.split(plugin["breaks"]), self.have) Index: pluginconf/flit.py ================================================================== --- pluginconf/flit.py +++ pluginconf/flit.py @@ -35,11 +35,11 @@
pyproject.toml foobar/__init__.py
 [build-system]
-requires = ["pluginconf", "flit]
+requires = ["pluginconf", "flit"]
 build-backend = "pluginconf.flit"
 
 [project]
 name = "foobar"
 dynamic = ["*"]

Index: pluginconf/gui.py
==================================================================
--- pluginconf/gui.py
+++ pluginconf/gui.py
@@ -50,30 +50,20 @@
     Where `plugin_states{}` is usually an entry in `config{}` itself. Depending on plugin
     and option names, it might even be a flat/shared namespace for both. Per default you'd
     set `files=["plugins/*.py", __file__]` to be read. But with `files=[]` it's possible to
     provide a `plugins=pluginconf.get_plugin_meta()` or prepared plugin/options dict instead.
 
-    Parameters
-    ----------
-    config : dict 🔁
-        Config settings, updated after dialog completion
-    plugin_states : dict 🔁
-        Plugin activation states, also input/output
-    files : list
-        Glob list of *.py files to extract meta definitions from
-    plugins : dict
-        Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected
-    opt_label : bool
-        Show config name= as label
-    theme : str
-        Set PSG window theme.
-    **kwargs : dict
-        Other options are passed on to PySimpleGUI
-
-    Returns
-    -------
-    True : if changed config{} values are to be saved (the dict will be updated in any case)
+    | Params        | | |
+    |---------------|---------|---------------------------------------------------------|
+    | config        | dict 🔁 | Config settings, updated after dialog completion        |
+    | plugin_states | dict 🔁 | Plugin activation states, also input/output             |
+    | files         | list    | Glob list of *.py files to extract meta definitions from|
+    | plugins       | dict    | Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected |
+    | opt_label     | bool    | Show config name= as label (instead of description)     |
+    | theme         | str     | Set PSG window theme.                                   |
+    | **kwargs      | dict    | Other options are passed on to PySimpleGUI              |
+    | **Returns**   | True    | if updated config{} values should be [Saved]            |
     """
     plugins = kwargs.get("plugins", {})
     opt_label = kwargs.get("opt_label", False)
     theme = kwargs.get("theme", "DefaultNoMoreNagging")
     if theme:
@@ -112,11 +102,11 @@
         return True
     #print(config, plugin_states)
 
 
 def plugin_layout(pmd_list, config, plugin_states, opt_label=False):
-    """ craft list of widgets for each read plugin """
+    """ craft list of widgets: \*( `plugin_entry`, \*`option_entry` )"""
     layout = []
     for plg in pmd_list:
         #print(plg.get("id"))
         layout = layout + plugin_entry(plg, plugin_states)
         for opt in plg["config"]:

Index: pluginconf/setup.py
==================================================================
--- pluginconf/setup.py
+++ pluginconf/setup.py
@@ -201,18 +201,15 @@
 def setup(filename=None, debug=False, **kwargs):
     """
     Wrapper around `setuptools.setup()` which adds some defaults
     and plugin meta data import, with some shortcut parameters:
 
-    Parameters
-    ----------
-    filename : str
-        main file "pkg/main.py" (else deduced from primary package name)
-    debug : bool
-        display collected options prior setuptools.setup() invocation
-    long_description : str
-        e.g. "README.md", else comment block used
+    | Parameters  | | |
+    |-------------|--------|------------------------------------------------|
+    | filename    | str    | main file "pkg/main.py" (else deduced from primary package name) |
+    | debug       | bool   | display collected options prior setuptools.setup() invocation |
+    |long_description| str | e.g. "README.md", else the comment block gets used |
 
     Other setup() params work as usual, and are passed trough. Notably
     entry_points= or data_files= can be used, even if they get augmented.
     """