Differences From Artifact [ce733ed382]:

To Artifact [fcff8bd0e5]:


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
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:
# 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:
#
#      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
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)
#
# 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.
#

#-- 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
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) 
            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
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
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))