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
| # 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,
|
|
|
| 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
| # 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 simpler setup:
#
# Create an empty plugins/__init__.py to use as package and for
# plugin discovery.
#
# Designate it as such:
#
# 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:
#
# conf = {
# "first_run": 1,
|
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
| # 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 --
"""
|
|
>
>
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
| 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
| # 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)
#
# 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 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 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.
#
#-- bit briefer for API docs --
"""
|
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
| 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):
|
|
>
| 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
| 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 functools
import pluginconf
#-- reset pluginconf if .bind is used
pluginconf.plugin_base = []
def load(name):
|
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
| |-------------|------------|-------------------------------|
| module | module/str | Package basename to later load plugins from |
| 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`.
"""
# if supplied as string, instead of active module
if isinstance(module, str):
|
|
| 164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
| |-------------|------------|-------------------------------|
| module | module/str | Package basename to later load plugins from |
| 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`.
"""
# if supplied as string, instead of active module
if isinstance(module, str):
|
276
277
278
279
280
281
282
283
284
285
| @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))
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
| 292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
| @staticmethod
def defaults():
""" *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))
|