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