Overview
Comment: | introduce .bind module |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
d5d59c76fd526556529a02ed98570381 |
User & Date: | mario on 2022-10-30 09:08:39 |
Other Links: | manifest | tags |
Context
2022-10-30
| ||
09:11 | rename module_base to data_root, change bytes.decode (probably should be a try/except), add dirname when module.__path__ missing, doc updates check-in: 8b8e58b0e7 user: mario tags: trunk | |
09:08 | introduce .bind module check-in: d5d59c76fd user: mario tags: trunk | |
2022-10-29
| ||
03:11 | table intro check-in: b2227cd919 user: mario tags: trunk | |
Changes
Added pluginconf/bind.py version [8927125c8f].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | # encoding: utf-8 # api: pluginconf ##type: loader # title: plugin loader # description: implements a trivial unified namespace importer # version: 0.1 # 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: # # Create an empty plugins/__init__.py to use as package and for # plugin discovery. # # Designate it as such: # # import pluginconf.bind # pluginconf.bind.base("plugins") # # Set up a default conf={} in your application, with some presets, # or updating it from a stored config file: # # conf = { # "first_run": 1, # "debug": 0, # "plugins": {}, # } # conf.update( # json.load(open("app.json", "r")) # ) # # Then update defaults from plugins: # # if conf["first_run"]: # pluginconf.bind.defaults(conf) # # 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 # register themselves. Alternatively use a class name aligned with # the plugin basename to disover it, or dir(), or similar such. # # 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() # 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: # # pluginconf.bind.base("plugins", dir="~/.config/app/usrplugs/") # pluginconf.bind.defaults(conf) # update # # With PySimpleGUI, `conf` could be a psg.UserSettings("app.json") for # automatic settings+update storage, btw. # #-- bit briefer for API docs -- """ Basic plugin loader implementation for flat namespaces. Ties together pluginconf lookups and config management for plugin loading in apps. It's rather basic, and subject to change. Does impose a config dict format, but no storage still. ### Usage example # designate a plugins/*.py package as plugin_base import plugins import pluginconf.bind pluginconf.bind.base(plugins) # preset core app settings / load from json, add plugin options conf = { "plugins": { } } pluginconf.bind.defaults(conf) # load conf-enabled plugins, and register modules somehow for mod in pluginconf.bind.load_enabled(conf): mod.init() ### 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) 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.) ---- Method interactions: ๐ = import, ๐งฉ = meta, ๐งพ = config, ๐ = setup """ import os import sys import importlib import pluginconf #-- reset pluginconf if .bind is used pluginconf.plugin_base = [] def load(name): """ Import individual plugin from any of the base paths ๐ Parameters ---------- name : str Plugin name in any of the registered plugin_baseยดs. (The whole namespace is assumed to be flat, and identifiers to be unique.) """ for package in pluginconf.plugin_base: if package in ("", "."): continue module_name = package + "." + name if module_name in sys.modules: return sys.modules[module_name] try: return importlib.import_module(module_name) except ImportError: pass def base(module, path=None): """ Register module as package/plugin_base. Or expand its search path ๐ . Parameters ---------- module : module/str The package basename to later load plugins from (must be a package, like `plugins/__init__.py`, or be tied to a path= or zip). Ideally this module was already imported in main. But parameter may be a string. path : str Add a directory or pyz/zip bundle to registered plugin_base. Could be invoked multiple times =./contrib/, =/usr/share/app/extenstions/, =~/.config/app/userplug/ (same as declaring the `__path__` in the base `package/__init__.py`.) """ # if supplied as string, instead of active module if isinstance(module, str): module = sys.modules.get(module) or __import__(module) # add to search list if module.__name__ not in pluginconf.plugin_base: pluginconf.plugin_base.append(module.__name__) # enjoin dir or pyz if not hasattr(module, "__path__"): module.__path__ = [_dirname(module.__file__)] if path: module.__path__.append(_dirname(path)) def find(**kwargs): """ Find plugins by e.g. type= or category= ๐งฉ Parameters ---------- type : str Usually you'd search on a designated plugin categorization, like type= and api=, or slot=, or class= or whatever is most consistent. Multiple attributes can be filtered on. (Version/title probably not useful here.) Returns ---------- dict : basename โ PluginMeta dict """ def has_all(pmd): for key, dep in kwargs.items(): if not pmd.get(key) == dep: break else: return True return pluginconf.PluginMeta({ name: pmd for name, pmd in pluginconf.all_plugin_meta().items() if has_all(pmd) }) def load_enabled(conf): """ Import modules that are enabled in conf[plugins]={name:True,โฆ} ๐งพ ๐ Parameters ---------- conf : dict Simple options-value dictionary, but with one conf["plugins"] = {} subdict, which holds plugin activation states. The config dict doesn't have to match the available plugin options (defaults can be added), but should focus on essential presets. Differentiation only might become relevant for storage. """ for name, state in conf.get("plugins", conf).items(): if not state: continue if name.startswith("_"): continue yield load(name) def defaults(conf): """ Traverse installed plugins and expand config dict with presets ๐งฉ ๐งพ Parameters ---------- conf : dict ๐ Expands the top-level config dict with preset values from any plugins. """ for name, pmd in pluginconf.all_plugin_meta().items(): pluginconf.add_plugin_defaults(conf, conf["plugins"], pmd, name) # pylint: disable=invalid-name class isolated(): """ Context manager for isolated plugin structures. ๐ This is a shallow alternative to pluginbase and library-level plugins. Temporarily swaps global settings, thus maps most static functions. with pluginconf.bind.isolated("libplugins") as bound: bound.modules2.init() print( bound.find(api="library") ) """ def __init__(self, package): self.package = package if isinstance(package, str) else package.__name__ self.reset = [] #mod = sys.modules.get(self.package) or __import__(self.package) # must be a real package #if not hasattr(mod, "__path__"): # mod.__path__ = [_dirname(mod.__file__)] def __enter__(self): """ just swap out global config """ self.reset, pluginconf.plugin_base = pluginconf.plugin_base, [] base(self.package) return self def __exit__(self, *exc): pluginconf.plugin_base = self.reset @staticmethod def load(name): """ load module from wrapped package ๐ """ return load(name) def __getattr__(self, name): return self.load(name) def get_data(self, *args, **kwargs): """ get file relative to encapsulated plugins ๐ """ pluginconf.get_data(*args, **kwargs, file_root=self.package) @staticmethod def find(**kwargs): """ find by meta attributes ๐งฉ """ return find(**kwargs) @staticmethod def defaults(): """ *return* defaults for isolated plugin structure ๐งฉ ๐งพ """ conf = {"plugins": {}} defaults(conf) return conf def _dirname(file): return os.path.dirname(os.path.realpath(file)) |
Added test/bind.py version [bdb0475b8c].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | # type: test # title: bind loader # description: simple loader mechanism # version: 0.1 # # Utilize plugins/ import pytest import logging logging.basicConfig(level=logging.DEBUG) import pluginconf print(pluginconf.plugin_base) import pluginconf.bind def init(reset): # pluginconf.plugin_base = [] import test.plugins pluginconf.bind.base(test.plugins) assert "test.plugins" in pluginconf.plugin_base assert test.plugins.__path__ != [] def test_first(): assert pluginconf.plugin_meta(module="first").title == "first plugin" def find_plugins(): assert len(pluginconf.bind.find(type="stub")) == 3 def config_set(): conf = { "first_run": 1, "plugins": { }, } pluginconf.bind.defaults(conf) assert conf["injected"] is True # converted from "1" assert conf["plugins"]["core"] == True # priority core def load_first(): conf = { "plugins": { "first": True, }, } for mod in pluginconf.bind.load_enabled(conf): assert mod.init def context(): with pluginconf.bind.isolated("test.plugins") as ext: assert len(ext.find(type="stub")) == 3 assert ext.run.init() == 4 def tearDown(reset): pass |
Modified test/conftest.py from [b78d8789f6] to [0ca86310bb].
1 2 | import pytest import pluginconf as pc | > > > > > > | 1 2 3 4 5 6 7 8 | import pytest import pluginconf as pc @pytest.fixture def reset(): pc.plugin_base = ["plugins"] pc.data_root = "config" |
Added test/plugins/__init__.py version [2b400b71e1].
> > > > | 1 2 3 4 | # type: virtual # title: plugins/* dir for bind test init = 1 |
Added test/plugins/core.py version [a74fa3ed69].
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | # type: stub # title: core # description: virtual main app # priority: core # version: 1 # category: value # config: # { name: core_opt, value: 123, type: int } # init = 3 |
Added test/plugins/first.py version [169f4b96c9].
> > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | # type: stub # title: first plugin # description: does nothing # version: 1 # category: value # config: # { name: injected, value: 1, type: bool } # init = 2 |
Added test/plugins/run.py version [115b4c2666].
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | # type: stub # title: runnable # description: run some code on import # priority: optional # version: 1 # category: code # config: - # def init(): return 4 |