pluginify 0.0.0.post1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pluginify/__init__.py +11 -0
- pluginify/_version.py +7 -0
- pluginify/commandline_typer.py +258 -0
- pluginify/config.py +144 -0
- pluginify/create_plugin_registries.py +1037 -0
- pluginify/errors.py +16 -0
- pluginify/interfaces/__init__.py +9 -0
- pluginify/interfaces/base.py +665 -0
- pluginify/interfaces/class_based/data_modifiers.py +38 -0
- pluginify/interfaces/class_based_plugin.py +210 -0
- pluginify/interfaces/yaml_based/configs.py +27 -0
- pluginify/plugin_registry.py +925 -0
- pluginify/plugins/classes/data_modifiers/cuboid.py +41 -0
- pluginify/plugins/yaml/configs/stucco.yaml +16 -0
- pluginify/pydantic_models/v1/configs.py +58 -0
- pluginify/utils/__init__.py +62 -0
- pluginify/utils/context_managers.py +32 -0
- pluginify-0.0.0.post1.dev0.dist-info/METADATA +66 -0
- pluginify-0.0.0.post1.dev0.dist-info/RECORD +22 -0
- pluginify-0.0.0.post1.dev0.dist-info/WHEEL +4 -0
- pluginify-0.0.0.post1.dev0.dist-info/entry_points.txt +6 -0
- pluginify-0.0.0.post1.dev0.dist-info/licenses/LICENSE +128 -0
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
# # # This source code is subject to the license referenced at
|
|
2
|
+
# # # https://github.com/NRLMMD-GEOIPS.
|
|
3
|
+
|
|
4
|
+
"""Generates all available plugins from installed packages under a given namespace.
|
|
5
|
+
|
|
6
|
+
After all plugins have been generated, they are written to a registered_plugins.json
|
|
7
|
+
file which contains a dictionary of all the registered plugins across all plugin
|
|
8
|
+
repositories under a given namespace.
|
|
9
|
+
|
|
10
|
+
Run 'pluginify create' to produce registered_plugins.json for
|
|
11
|
+
EVERY currently installed plugin package under a given namespace
|
|
12
|
+
(default='pluginify.plugin_packages'). A separate registered_plugins.json is created at
|
|
13
|
+
the top level package directory for each plugin package under that namespace.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from importlib import metadata, resources, util, import_module
|
|
18
|
+
from inspect import signature
|
|
19
|
+
import json
|
|
20
|
+
from os import makedirs, remove
|
|
21
|
+
from os.path import (
|
|
22
|
+
basename,
|
|
23
|
+
dirname,
|
|
24
|
+
exists,
|
|
25
|
+
join as os_join,
|
|
26
|
+
relpath as os_relpath,
|
|
27
|
+
split,
|
|
28
|
+
splitext,
|
|
29
|
+
)
|
|
30
|
+
import re
|
|
31
|
+
import warnings
|
|
32
|
+
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
from pluginify.config import get_registry_cache_dir
|
|
36
|
+
from pluginify.errors import PluginRegistryError
|
|
37
|
+
|
|
38
|
+
LOG = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_docstring(docstring, use_regex=True):
|
|
42
|
+
"""Format the provided docstring placement in the plugin registry.
|
|
43
|
+
|
|
44
|
+
Found when using the CLI and inspecting the registry, some docstrings are formatted
|
|
45
|
+
in a hard to read manner and look pretty bad. This function will format these
|
|
46
|
+
docstrings to be easily readable, whether obtained via the CLI or manually inspected
|
|
47
|
+
in the plugin registry.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
docstring: str
|
|
52
|
+
- The docstring which we are going to format.
|
|
53
|
+
use_regex: bool, optional (default=False)
|
|
54
|
+
- Whether or not we want to apply regex formatting to the docstring. Usually
|
|
55
|
+
recommended as it will replace 'newline' chars but not purposeful
|
|
56
|
+
'.newline' strings.
|
|
57
|
+
"""
|
|
58
|
+
if docstring:
|
|
59
|
+
if use_regex:
|
|
60
|
+
# Regex pattern for subbing out "\n" but not ".\n"
|
|
61
|
+
pattern = r"(?<!\.)\n"
|
|
62
|
+
docstring = re.sub(
|
|
63
|
+
pattern,
|
|
64
|
+
" ",
|
|
65
|
+
docstring.strip().replace("\n\n", "\n"),
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
docstring = docstring.strip().replace("\n\n", "\n")
|
|
69
|
+
return docstring
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def remove_registries(namespace, plugin_packages):
|
|
73
|
+
"""Remove all plugin registries if a PluginRegistryError is raised.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
namespace: str
|
|
78
|
+
Namespace that your plugin packages fall under. The argument parser defaults
|
|
79
|
+
this value to 'pluginify.plugin_packages', but a user can create separate
|
|
80
|
+
namespaces if developing interfaces outside of pluginify.
|
|
81
|
+
plugin_packages: list[EntryPoints]
|
|
82
|
+
A list of EntryPoints pointing to each installed package --> ie.
|
|
83
|
+
[EntryPoint(name='pluginify', value='pluginify',
|
|
84
|
+
group='pluginify.plugin_packages'), ...]
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
None
|
|
89
|
+
"""
|
|
90
|
+
# Remove registries is called whenever an improperly formatted plugin or
|
|
91
|
+
# package is encountered. This is not called until all errors have been
|
|
92
|
+
# collected and reported, to facilitate rapidly identifying and resolving
|
|
93
|
+
# errors.
|
|
94
|
+
LOG.info(
|
|
95
|
+
"\n\n\n\nERROR: Removing registries due to improperly formatted plugins.\n"
|
|
96
|
+
"You must fix the error(s) shown below before pluginify can operate correctly."
|
|
97
|
+
"\nOnce fixed, please run 'pluginify create' to set up your plugins "
|
|
98
|
+
"appropriately\n\n\n"
|
|
99
|
+
)
|
|
100
|
+
# Remove registered_plugins.yaml and registered_plugins.json if they exist
|
|
101
|
+
# for each plugin package.
|
|
102
|
+
for pkg in plugin_packages:
|
|
103
|
+
write_dir = get_registry_cache_dir(namespace, pkg.value)
|
|
104
|
+
yaml_plug_path = str(write_dir / "registered_plugins.yaml")
|
|
105
|
+
json_plug_path = str(write_dir / "registered_plugins.json")
|
|
106
|
+
if exists(yaml_plug_path):
|
|
107
|
+
remove(yaml_plug_path)
|
|
108
|
+
if exists(json_plug_path):
|
|
109
|
+
remove(json_plug_path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def registry_sanity_check(plugin_packages, save_type, namespace):
|
|
113
|
+
"""Check that each plugin package registry has no duplicate lowest depth entries.
|
|
114
|
+
|
|
115
|
+
If it does, raise a `PluginRegistryError` for that specific package, then remove all
|
|
116
|
+
plugin registries from each package so the user must fix the error before
|
|
117
|
+
continuing. While this doesn't cause a normal error, duplicate plugins will be
|
|
118
|
+
overwritten by same-named plugin found in the last package-entrypoint.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
plugin_packages: list EntryPoints
|
|
123
|
+
A list of EntryPoints pointing to each installed package --> ie.
|
|
124
|
+
`[EntryPoint(name='pluginify', value='pluginify',
|
|
125
|
+
group='pluginify.plugin_packages')]`
|
|
126
|
+
save_type: str
|
|
127
|
+
The file format to save to `[json, yaml]`
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
No returns
|
|
132
|
+
|
|
133
|
+
Exceptions
|
|
134
|
+
----------
|
|
135
|
+
PluginRegistryError
|
|
136
|
+
If `error_message` has contents, then raise PluginRegistryError(error_message).
|
|
137
|
+
The `error_message` string will collect and report on all errors within
|
|
138
|
+
this function prior to raising the `PluginRegistryError` to facilitate rapidly
|
|
139
|
+
identifying and resolving errors throughout all plugin packages.
|
|
140
|
+
"""
|
|
141
|
+
error_message = ""
|
|
142
|
+
# comp_pkg is the package being compared against. This package is compared
|
|
143
|
+
# against every other available package that is installed.
|
|
144
|
+
for comp_idx, comp_pkg in enumerate(plugin_packages):
|
|
145
|
+
# yaml output is used primarily for testing purposes (since it is more human
|
|
146
|
+
# readable than json), and json output is used for processing. Ensure we
|
|
147
|
+
# can load either option.
|
|
148
|
+
comp_write_dir = get_registry_cache_dir(namespace, comp_pkg.value)
|
|
149
|
+
if save_type == "yaml":
|
|
150
|
+
registry_fname = comp_write_dir / "registered_plugins.yaml"
|
|
151
|
+
with open(registry_fname, "r") as reg_file:
|
|
152
|
+
comp_registry = yaml.safe_load(reg_file)
|
|
153
|
+
else:
|
|
154
|
+
# json.load is much faster than yaml.safe_load
|
|
155
|
+
registry_fname = comp_write_dir / "registered_plugins.json"
|
|
156
|
+
with open(registry_fname, "r") as reg_file:
|
|
157
|
+
comp_registry = json.load(reg_file)
|
|
158
|
+
for pkg_idx, pkg in enumerate(plugin_packages):
|
|
159
|
+
# pkg is the package being compared against comp_pkg. For example, if
|
|
160
|
+
# comp_pkg was 'pluginify', then it would compare against all other packages
|
|
161
|
+
# registered under the pluginify.plugin_packages namespace.
|
|
162
|
+
|
|
163
|
+
# The if statement below checks the index of pkg_idx and comp_idx.
|
|
164
|
+
# If pkg_idk <= comp_idx, that means it's either the same package as
|
|
165
|
+
# comp_pkg, or that the comparison has already been performed.
|
|
166
|
+
if pkg_idx <= comp_idx:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
write_dir = get_registry_cache_dir(namespace, pkg.value)
|
|
170
|
+
# Track sets of plugins by plugin type
|
|
171
|
+
# (text, yaml_based, and class_based)
|
|
172
|
+
if save_type == "yaml":
|
|
173
|
+
registry_fname = write_dir / "registered_plugins.yaml"
|
|
174
|
+
with open(registry_fname, "r") as reg_file:
|
|
175
|
+
pkg_registry = yaml.safe_load(reg_file)
|
|
176
|
+
else:
|
|
177
|
+
registry_fname = write_dir / "registered_plugins.json"
|
|
178
|
+
with open(registry_fname, "r") as reg_file:
|
|
179
|
+
pkg_registry = json.load(reg_file)
|
|
180
|
+
for plugin_type in list(pkg_registry.keys()):
|
|
181
|
+
# check the pkg's registry for both yaml-based and class-based plugins
|
|
182
|
+
for interface in comp_registry[plugin_type]:
|
|
183
|
+
# check each interface of comp_pkg
|
|
184
|
+
# for each type of plugin (yaml/class)-based
|
|
185
|
+
if interface in pkg_registry[plugin_type]:
|
|
186
|
+
# if this interface is also in the pkg_registry, then get the
|
|
187
|
+
# dictionary of comp_plugins for that interface
|
|
188
|
+
comp_plugins = comp_registry[plugin_type][interface]
|
|
189
|
+
for plugin in comp_plugins:
|
|
190
|
+
# for each plugin in comp_plugins dict
|
|
191
|
+
if plugin in pkg_registry[plugin_type][interface]:
|
|
192
|
+
# if this plugin is also in the pkg_registry's
|
|
193
|
+
# corresponding interface, then retrieve that plugin
|
|
194
|
+
pkg_plugin = pkg_registry[plugin_type][interface][
|
|
195
|
+
plugin
|
|
196
|
+
]
|
|
197
|
+
comp_plugin = comp_registry[plugin_type][interface][
|
|
198
|
+
plugin
|
|
199
|
+
]
|
|
200
|
+
if "relpath" in pkg_plugin:
|
|
201
|
+
# If 'relpath' is found in the plugin, raise a
|
|
202
|
+
# PluginRegistryError, and remove the registries.
|
|
203
|
+
# We do this because 'family' is a top level
|
|
204
|
+
# attribute on all plugins, and means you're at
|
|
205
|
+
# the lowest depth of the Plugin entry, meaning
|
|
206
|
+
# there are two Plugins with Duplicate Names.
|
|
207
|
+
error_message += """Error with packages [{}, {}]:
|
|
208
|
+
You can't have two Plugins of the same
|
|
209
|
+
interface [{}] with the same
|
|
210
|
+
plugin name [{}]
|
|
211
|
+
pkg relpath: {}
|
|
212
|
+
comp relpath: {}""".format(
|
|
213
|
+
comp_pkg.value,
|
|
214
|
+
pkg.value,
|
|
215
|
+
interface,
|
|
216
|
+
plugin,
|
|
217
|
+
pkg_plugin["relpath"],
|
|
218
|
+
comp_plugin["relpath"],
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
# If the statement above is false,
|
|
222
|
+
# that means the plugin
|
|
223
|
+
# we are dealing with is 'Product'-based.
|
|
224
|
+
# This means there are subplugins that we
|
|
225
|
+
# need to check against their defind
|
|
226
|
+
# source names. Grab the comparsion
|
|
227
|
+
# Product Plugin.
|
|
228
|
+
for sub_plg in comp_plugin:
|
|
229
|
+
# Loop through each sub-plugin of the
|
|
230
|
+
# comparison product plugin.
|
|
231
|
+
if sub_plg in pkg_plugin:
|
|
232
|
+
# If this sub-plugin is also in
|
|
233
|
+
# the package Product plugin,
|
|
234
|
+
# raise a PluginRegistryError
|
|
235
|
+
# and remove the registries.
|
|
236
|
+
pkg_relpath = "not defined"
|
|
237
|
+
if "relpath" in pkg_plugin:
|
|
238
|
+
pkg_relpath = pkg_plugin["relpath"]
|
|
239
|
+
subplg_relpath = "not defined"
|
|
240
|
+
if "relpath" in sub_plg:
|
|
241
|
+
subplg_relpath = sub_plg["relpath"]
|
|
242
|
+
error_message += """
|
|
243
|
+
Error with packages:
|
|
244
|
+
[{}, {}]:
|
|
245
|
+
You can't have two products of the same
|
|
246
|
+
interface [{}] with the same
|
|
247
|
+
plugin name [{}] found under
|
|
248
|
+
sub_plg name [{}]
|
|
249
|
+
relpath: {}
|
|
250
|
+
sub_plg relpath: {}\n""".format(
|
|
251
|
+
comp_pkg.value,
|
|
252
|
+
pkg.value,
|
|
253
|
+
interface,
|
|
254
|
+
sub_plg,
|
|
255
|
+
plugin,
|
|
256
|
+
pkg_relpath,
|
|
257
|
+
subplg_relpath,
|
|
258
|
+
)
|
|
259
|
+
if error_message:
|
|
260
|
+
remove_registries(namespace, plugin_packages)
|
|
261
|
+
raise PluginRegistryError(error_message)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def check_plugin_exists(package, plugins, interface_name, plugin_name, relpath):
|
|
265
|
+
"""Check if plugin already exists. If it does raise a `PluginRegistryError`.
|
|
266
|
+
|
|
267
|
+
Note this only checks for duplicate plugins within a single plugin package.
|
|
268
|
+
The `registry_sanity_check` function is used after all plugins have been
|
|
269
|
+
loaded to identify duplicate plugins across different plugin packages.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
package: str
|
|
274
|
+
The package being tested against
|
|
275
|
+
plugins: dict
|
|
276
|
+
A dictionary object of all installed plugins in the current plugin package.
|
|
277
|
+
interface_name: str
|
|
278
|
+
A string representing the interface being checked against
|
|
279
|
+
plugin_name: str
|
|
280
|
+
A string representing the name of the plugin within the interface
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
error_message : str
|
|
285
|
+
Empty string if no error, appropriate informative error message if
|
|
286
|
+
duplicate plugin found in current plugin package.
|
|
287
|
+
"""
|
|
288
|
+
# Check if the passed in plugin_name is already in the current plugin
|
|
289
|
+
# package dictionary for this interface.
|
|
290
|
+
if plugin_name in plugins[interface_name]:
|
|
291
|
+
return f"""\nError in package [{package}]:
|
|
292
|
+
You can not have two Plugins of the same
|
|
293
|
+
interface [{interface_name}] with the same
|
|
294
|
+
name [{plugin_name}] found at
|
|
295
|
+
relpath [{plugins[interface_name][plugin_name]["relpath"]}] and
|
|
296
|
+
relpath [{relpath}]
|
|
297
|
+
"""
|
|
298
|
+
return ""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def write_registered_plugins(pkg_dir, plugins, save_type):
|
|
302
|
+
"""Write dictionary of all plugins available from installed packages.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
pkg_dir: str
|
|
307
|
+
Path in which to write registered_plugins
|
|
308
|
+
plugins: dict
|
|
309
|
+
A dictionary object of all installed package plugins
|
|
310
|
+
save_type: str
|
|
311
|
+
The file format to save to [json, yaml]
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
No returns, file written to `pkg_dir`
|
|
316
|
+
"""
|
|
317
|
+
if save_type == "yaml":
|
|
318
|
+
reg_plug_abspath = os_join(pkg_dir, "registered_plugins.yaml")
|
|
319
|
+
with open(reg_plug_abspath, "w") as plugin_registry:
|
|
320
|
+
LOG.info("Writing %s", reg_plug_abspath)
|
|
321
|
+
yaml.safe_dump(plugins, plugin_registry, default_flow_style=False)
|
|
322
|
+
else:
|
|
323
|
+
reg_plug_abspath = os_join(pkg_dir, "registered_plugins.json")
|
|
324
|
+
with open(reg_plug_abspath, "w") as plugin_registry:
|
|
325
|
+
LOG.info("Writing %s", reg_plug_abspath)
|
|
326
|
+
json.dump(plugins, plugin_registry, indent=4)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def create_plugin_registries(plugin_packages, save_type, namespace):
|
|
330
|
+
"""Generate all plugin paths associated with every installed packages.
|
|
331
|
+
|
|
332
|
+
These paths include text plugins, class_based plugins
|
|
333
|
+
and normal YAML plugins. After these paths are generated, they are sent
|
|
334
|
+
to parse_plugin_paths, which generates and adds the actual plugins to the
|
|
335
|
+
plugins dictionary.
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
plugin_packages: list EntryPoints
|
|
340
|
+
A list of EntryPoints pointing to each installed package --> ie.
|
|
341
|
+
[EntryPoint(name='pluginify', value='pluginify',
|
|
342
|
+
group='pluginify.plugin_packages'), ...]
|
|
343
|
+
save_type: str
|
|
344
|
+
The file format to save to [json, yaml]
|
|
345
|
+
namespace: str
|
|
346
|
+
Namespace that your plugin packages fall under. The argument parser defaults
|
|
347
|
+
this value to 'pluginify.plugin_packages', but a user can create separate
|
|
348
|
+
namespaces if developing interfaces outside of pluginify.
|
|
349
|
+
"""
|
|
350
|
+
# It appears when there is *.egg-info directory, it picks that package up
|
|
351
|
+
# twice in the list. If the same package path exists twice, only keep one
|
|
352
|
+
# of them. This appears to be a bug with Python 3.9 entry points.
|
|
353
|
+
pkg_dirs = []
|
|
354
|
+
unique_package_entry_points = []
|
|
355
|
+
# Note we only use ep.value (resources.files finds the actual plugins),
|
|
356
|
+
# so we do not need to worry about saving the "wrong" package here.
|
|
357
|
+
# We are actually looping through all the files in each package, so
|
|
358
|
+
# we do not have an entry point for every plugin, just a single entry
|
|
359
|
+
# point for each plugin package.
|
|
360
|
+
for ep in plugin_packages:
|
|
361
|
+
pkg_dir = str(resources.files(ep.value))
|
|
362
|
+
if pkg_dir not in pkg_dirs:
|
|
363
|
+
pkg_dirs += [pkg_dir]
|
|
364
|
+
# Grab the unique package entry points.
|
|
365
|
+
unique_package_entry_points += [ep]
|
|
366
|
+
|
|
367
|
+
error_message = ""
|
|
368
|
+
# Loop through only the unique package entry points to avoid duplicate
|
|
369
|
+
# plugin errors.
|
|
370
|
+
for pkg in unique_package_entry_points:
|
|
371
|
+
# This is passed by reference and populated with each call to parse
|
|
372
|
+
# plugin packages.
|
|
373
|
+
plugins = {
|
|
374
|
+
"text_based": {},
|
|
375
|
+
"yaml_based": {},
|
|
376
|
+
"class_based": {},
|
|
377
|
+
}
|
|
378
|
+
# Track sets of plugins by plugin type
|
|
379
|
+
# (text_based, yaml_based, and class_based)
|
|
380
|
+
package = pkg.value
|
|
381
|
+
LOG.debug("package == " + str(package))
|
|
382
|
+
# We are specifically looping through all files in the ``plugins``
|
|
383
|
+
# directory within the plugin package. Potentially we may want
|
|
384
|
+
# to update this in the future to actually include package_plugins
|
|
385
|
+
# and package_schema entry points, and point the pyproject.toml
|
|
386
|
+
# package_plugins entry point directly to the appropriate directory
|
|
387
|
+
# that holds all plugins, and the package_schema entry point directly
|
|
388
|
+
# to the directory that holds all scheam, so the subdirectories are
|
|
389
|
+
# not hard coded here in the create_plugin_registries code.
|
|
390
|
+
pkg_plugin_path = resources.files(package) / "plugins"
|
|
391
|
+
pkg_dir = str(resources.files(package))
|
|
392
|
+
# Generate a path to the parent directory where we should write the plugin
|
|
393
|
+
# registry file for this package to.
|
|
394
|
+
write_dir = get_registry_cache_dir(namespace, package)
|
|
395
|
+
# Create the directories of 'write_dir' recursively. If they already exist,
|
|
396
|
+
# that's ok.
|
|
397
|
+
if not exists(write_dir):
|
|
398
|
+
makedirs(write_dir, exist_ok=True)
|
|
399
|
+
# Grab all YAML, Python, and txt files within the plugins directory.
|
|
400
|
+
# YAML schema files may be supported in the future.
|
|
401
|
+
yaml_files = pkg_plugin_path.rglob("*.yaml")
|
|
402
|
+
python_files = pkg_plugin_path.rglob("*.py")
|
|
403
|
+
text_files = pkg_plugin_path.rglob("*.txt")
|
|
404
|
+
# plugin_paths dictionary contains lists of files for each plugin
|
|
405
|
+
# type (ie, yaml based, text based, and class based plugins, and
|
|
406
|
+
# in the future potentially schema)
|
|
407
|
+
plugin_paths = {
|
|
408
|
+
"yaml": sorted(yaml_files),
|
|
409
|
+
"text": text_files,
|
|
410
|
+
"py_files": python_files,
|
|
411
|
+
}
|
|
412
|
+
# `plugins` is passed by reference and populated with all YAML, text, and
|
|
413
|
+
# python plugins found within the current plugin package `package`.
|
|
414
|
+
# This dictionary is formatted appropriately to be written out to the
|
|
415
|
+
# plugin registry file as either a json or YAML output.
|
|
416
|
+
# If any errors are found, append the error message string to the current
|
|
417
|
+
# error_message. Do not raise an exception until all plugins have been
|
|
418
|
+
# read in, so we can collect and report on all errors at once.
|
|
419
|
+
error_message += parse_plugin_paths(
|
|
420
|
+
plugin_paths, package, pkg_dir, plugins, namespace
|
|
421
|
+
)
|
|
422
|
+
LOG.debug("Available Plugin Types:\n" + str(plugins.keys()))
|
|
423
|
+
LOG.debug(
|
|
424
|
+
"Available YAML Plugin Interfaces:\n" + str(plugins["yaml_based"].keys())
|
|
425
|
+
)
|
|
426
|
+
LOG.debug(
|
|
427
|
+
"Available Class Plugin Interfaces:\n" + str(plugins["class_based"].keys())
|
|
428
|
+
)
|
|
429
|
+
# Write the current plugin dictionary to the registered plugins file.
|
|
430
|
+
write_registered_plugins(write_dir, plugins, save_type)
|
|
431
|
+
# If error_message is not the empty string, that means we had some errors,
|
|
432
|
+
# so handle appropriately.
|
|
433
|
+
if error_message:
|
|
434
|
+
# Remove all registries to prevent running with an incomplete
|
|
435
|
+
# or corrupt set of plugins. Force user to resolve errors before
|
|
436
|
+
# proceeding.
|
|
437
|
+
remove_registries(namespace, metadata.entry_points(group=namespace))
|
|
438
|
+
# Now raise the error, including the error message with output
|
|
439
|
+
# from every failed plugin/file during the attempted registry process.
|
|
440
|
+
raise PluginRegistryError(error_message)
|
|
441
|
+
# Above error only occurs for duplicates within a single registry.
|
|
442
|
+
# registry_sanity_check will check for duplicates across all
|
|
443
|
+
# registries.
|
|
444
|
+
registry_sanity_check(unique_package_entry_points, save_type, namespace)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def parse_plugin_paths(plugin_paths, package, package_dir, plugins, namespace):
|
|
448
|
+
"""Parse the plugin_paths provided from the current installed package.
|
|
449
|
+
|
|
450
|
+
Then, add them to the plugins dictionary based on the path of the plugin.
|
|
451
|
+
The path contains information as to whether the plugin is a class_based, text_based,
|
|
452
|
+
or a yaml_based plugin.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
plugin_paths: dict
|
|
457
|
+
A dictionary of filepaths, with keys referring to the type of plugin
|
|
458
|
+
package: str
|
|
459
|
+
The current package being parsed
|
|
460
|
+
package_dir: str
|
|
461
|
+
The path to the current package (for determining relative paths)
|
|
462
|
+
plugins: dict
|
|
463
|
+
A dictionary object of all installed package plugins
|
|
464
|
+
namespace: str
|
|
465
|
+
Namespace that your plugin packages fall under. The argument parser defaults
|
|
466
|
+
this value to 'pluginify.plugin_packages', but a user can create separate
|
|
467
|
+
namespaces if developing interfaces outside of pluginify.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
error_message: str
|
|
472
|
+
String containing informative error messages from any plugins that
|
|
473
|
+
were improperly formatted. An exception will be raised at the
|
|
474
|
+
very end if error_message is not the empty string - this allows
|
|
475
|
+
collecting ALL errors throughout the plugin registry process and
|
|
476
|
+
reporting them all at once, to facilitate rapidly identifying and
|
|
477
|
+
resolving errors.
|
|
478
|
+
"""
|
|
479
|
+
error_message = ""
|
|
480
|
+
# Loop through each plugin type, ie, text, yaml, class, and later schema.
|
|
481
|
+
for plugin_type in plugin_paths:
|
|
482
|
+
# Loop through each file of the current plugin type.
|
|
483
|
+
for filepath in plugin_paths[plugin_type]:
|
|
484
|
+
filepath = str(filepath)
|
|
485
|
+
# Path relative to the package directory
|
|
486
|
+
relpath = os_relpath(filepath, start=package_dir)
|
|
487
|
+
# plugins is passed by reference, so any new plugins found
|
|
488
|
+
# are added to the plugins dictionary and retained throughout.
|
|
489
|
+
if plugin_type == "yaml": # yaml based plugins
|
|
490
|
+
error_message += add_yaml_plugin(
|
|
491
|
+
filepath, relpath, package, plugins["yaml_based"], namespace
|
|
492
|
+
)
|
|
493
|
+
# Ensure we append any errors to the error_message as we go.
|
|
494
|
+
# Exception will not be raised until the very end, when we
|
|
495
|
+
# have collected ALL errors. This makes it easier to fix
|
|
496
|
+
# all the errors at once.
|
|
497
|
+
elif plugin_type == "text":
|
|
498
|
+
error_message += add_text_plugin(
|
|
499
|
+
package, relpath, plugins["text_based"]
|
|
500
|
+
)
|
|
501
|
+
# Potentially support schema in the future.
|
|
502
|
+
# elif plugin_type == "schemas": # schema based yamls
|
|
503
|
+
# add_schema_plugin(
|
|
504
|
+
# filepath, abspath, relpath, package, plugins["schemas"]
|
|
505
|
+
# )
|
|
506
|
+
else: # Python files; class based plugins
|
|
507
|
+
error_message += add_class_plugin(
|
|
508
|
+
package, relpath, plugins["class_based"]
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Ensure we return a string error_message with ALL errors appended.
|
|
512
|
+
# This will be raised at the end if error_message has any contents.
|
|
513
|
+
return error_message
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def add_yaml_plugin(filepath, relpath, package, plugins, namespace):
|
|
517
|
+
"""Add the yaml plugin associated with the filepaths and package to plugins.
|
|
518
|
+
|
|
519
|
+
Parameters
|
|
520
|
+
----------
|
|
521
|
+
filepath: str
|
|
522
|
+
The path of the plugin derived from resouces.files(package) / plugin
|
|
523
|
+
relpath: str
|
|
524
|
+
The relative path to the filepath provided
|
|
525
|
+
package: str
|
|
526
|
+
The current package being parsed
|
|
527
|
+
plugins: dict
|
|
528
|
+
A dictionary object of all installed package plugins
|
|
529
|
+
namespace: str
|
|
530
|
+
Namespace that your plugin packages fall under. The argument parser defaults
|
|
531
|
+
this value to 'pluginify.plugin_packages', but a user can create separate
|
|
532
|
+
namespaces if developing interfaces outside of pluginify.
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
error_message: str
|
|
537
|
+
String containing informative error messages from any plugins that
|
|
538
|
+
were improperly formatted. An exception will be raised at the
|
|
539
|
+
very end if error_message is not the empty string - this allows
|
|
540
|
+
collecting ALL errors throughout the plugin registry process and
|
|
541
|
+
reporting them all at once, to facilitate rapidly identifying and
|
|
542
|
+
resolving errors.
|
|
543
|
+
"""
|
|
544
|
+
# Since this function always loads all plugins, loading them up front should be
|
|
545
|
+
# fine. It might be good to refactor this at some point. This is a quick fix.
|
|
546
|
+
with open(filepath, "r") as plugin_file:
|
|
547
|
+
new_plugins = list(yaml.safe_load_all(plugin_file))
|
|
548
|
+
|
|
549
|
+
for plugin in new_plugins:
|
|
550
|
+
plugin["relpath"] = relpath
|
|
551
|
+
plugin["package"] = package
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
interface_name = plugin["interface"]
|
|
555
|
+
except KeyError:
|
|
556
|
+
raise PluginRegistryError(f"""No 'interface' level in '{filepath}'.
|
|
557
|
+
Ensure all required metadata is included.""")
|
|
558
|
+
|
|
559
|
+
if namespace != "geoips.plugin_packages":
|
|
560
|
+
mod = import_module(package)
|
|
561
|
+
interface_module = getattr(mod.interfaces, f"{interface_name}")
|
|
562
|
+
else:
|
|
563
|
+
# This is buried to avoid circular import errors. For some reason, doing
|
|
564
|
+
# this at the top level of the module with 'import_optional_dependencies'
|
|
565
|
+
# does not always work. If this is hit more than once, the cached import
|
|
566
|
+
# will be used. Efficiency is not impacted.
|
|
567
|
+
try:
|
|
568
|
+
import geoips.interfaces
|
|
569
|
+
except ImportError as e:
|
|
570
|
+
raise RuntimeError(
|
|
571
|
+
"The 'geoips' package is required but could not be imported."
|
|
572
|
+
"Please install geoips before running `pluginify create` again."
|
|
573
|
+
) from e
|
|
574
|
+
interface_module = getattr(geoips.interfaces, f"{interface_name}")
|
|
575
|
+
|
|
576
|
+
if interface_name not in plugins.keys():
|
|
577
|
+
plugins[interface_name] = {}
|
|
578
|
+
|
|
579
|
+
error_message = ""
|
|
580
|
+
# If the current family is "list", make sure we loop through the list,
|
|
581
|
+
# expanding out each individual product found within the list.
|
|
582
|
+
if plugin["family"] == "list":
|
|
583
|
+
# These are not complete plugins at this stage, only the metadata,
|
|
584
|
+
# so do not validate the plugins here, they will be validated on open.
|
|
585
|
+
|
|
586
|
+
# plugin_yaml_to_obj returns the actual plugin object, ie, ProductsPlugin,
|
|
587
|
+
# or SectorsPlugin, etc.
|
|
588
|
+
plg_list = interface_module._plugin_yaml_to_obj(plugin["name"], plugin)
|
|
589
|
+
|
|
590
|
+
# plg_list is an e.g. ProductsPlugin object of family list,
|
|
591
|
+
# so within its e.g. plg_list["spec"]["products"] key, we will
|
|
592
|
+
# find a list of all products contained within this single
|
|
593
|
+
# plugin file.
|
|
594
|
+
for yaml_subplg in plg_list["spec"][interface_module.name]:
|
|
595
|
+
# _create_registered_plugin_names will return a list of names
|
|
596
|
+
# found in this single product specification. Most interfaces
|
|
597
|
+
# this will just return a list of length one, containing
|
|
598
|
+
# [plugin.name], but for products it will return a list of
|
|
599
|
+
# tuples of (source_name, product_name), allowing specifying
|
|
600
|
+
# a list of valid sources within each product spec.
|
|
601
|
+
subplg_names = interface_module._create_registered_plugin_names(
|
|
602
|
+
yaml_subplg
|
|
603
|
+
)
|
|
604
|
+
# Loop through each of the returned registered plugin names.
|
|
605
|
+
# Give each one its own entry in the plugin registry for easy
|
|
606
|
+
# access.
|
|
607
|
+
for subplg_name in subplg_names:
|
|
608
|
+
subplg_source = str(subplg_name[0])
|
|
609
|
+
subplg_product = str(subplg_name[1])
|
|
610
|
+
if subplg_source not in plugins[interface_name]:
|
|
611
|
+
plugins[interface_name][subplg_source] = {}
|
|
612
|
+
# since we are dealing with sub-plugins of a product plugin,
|
|
613
|
+
# include a couple other pieces of information, such as
|
|
614
|
+
# product_defaults and source_names.
|
|
615
|
+
source_names = yaml_subplg["source_names"]
|
|
616
|
+
pd = None
|
|
617
|
+
if "product_defaults" in yaml_subplg:
|
|
618
|
+
pd = yaml_subplg["product_defaults"]
|
|
619
|
+
family = None
|
|
620
|
+
if "family" in yaml_subplg:
|
|
621
|
+
family = yaml_subplg["family"]
|
|
622
|
+
docstring = None
|
|
623
|
+
if "docstring" in yaml_subplg:
|
|
624
|
+
docstring = yaml_subplg["docstring"]
|
|
625
|
+
# If docstring or family are not specified, and a
|
|
626
|
+
# product_defaults isn't specified, raise an error
|
|
627
|
+
# (docstring and family must be defined in a
|
|
628
|
+
# product_defaults if not defined explicitly).
|
|
629
|
+
if (not docstring or not family) and not pd:
|
|
630
|
+
error_message += f"""
|
|
631
|
+
Error with package '{plugin["package"]}':
|
|
632
|
+
docstring or family not defined in product,
|
|
633
|
+
and product_defaults not specified.
|
|
634
|
+
Must specify docstring and family in either product
|
|
635
|
+
or product_defaults.
|
|
636
|
+
interface '{interface_module.name}'
|
|
637
|
+
plugin name '{plugin["name"]}'
|
|
638
|
+
pkg relpath: '{plugin["relpath"]}'\n"""
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
# I think filling in the nulls should be handled at the CLI
|
|
642
|
+
# and/or interface level, so leave family/docstring as None
|
|
643
|
+
# in the registry.
|
|
644
|
+
# There is no way to guarantee the product_defaults are
|
|
645
|
+
# available in the same plugin package as the current plugin,
|
|
646
|
+
# so pulling the family and docstring from the product defaults
|
|
647
|
+
# when creating the registry will not always work.
|
|
648
|
+
# If we are concerned about efficiency with opening product and
|
|
649
|
+
# product_defaults with every plugin access, we could do a
|
|
650
|
+
# second pass in the plugin registry creation to fill in the
|
|
651
|
+
# nulls (but we will not worry about that yet)
|
|
652
|
+
# if not docstring or not family:
|
|
653
|
+
# # if the yaml_sub_plg doesn't include a docstring, grab its
|
|
654
|
+
# # product_defaults docstring
|
|
655
|
+
# if (
|
|
656
|
+
# "product_defaults" not in plugins
|
|
657
|
+
# or pd not in plugins["product_defaults"]
|
|
658
|
+
# ):
|
|
659
|
+
# LOG.error(
|
|
660
|
+
# f"""Product defaults '{pd}' does not exist.
|
|
661
|
+
# Using 'undefined' docstring.
|
|
662
|
+
# Need to figure out how to pull product defaults
|
|
663
|
+
# from a different plugin package at some point.""" # NOQA
|
|
664
|
+
# )
|
|
665
|
+
# else:
|
|
666
|
+
# if not docstring:
|
|
667
|
+
# docstring = plugins["product_defaults"][pd]["docstring"] # NOQA
|
|
668
|
+
# if not family:
|
|
669
|
+
# family = plugins["product_defaults"][pd]["family"]
|
|
670
|
+
plugins[interface_name][subplg_source][subplg_product] = {
|
|
671
|
+
"docstring": str(format_docstring(docstring)),
|
|
672
|
+
"family": family,
|
|
673
|
+
"interface": interface_module.name,
|
|
674
|
+
"package": plugin["package"],
|
|
675
|
+
"plugin_type": "yaml_based",
|
|
676
|
+
"product_defaults": pd,
|
|
677
|
+
"source_names": source_names,
|
|
678
|
+
"relpath": plugin["relpath"],
|
|
679
|
+
}
|
|
680
|
+
else:
|
|
681
|
+
error_message += check_plugin_exists(
|
|
682
|
+
package, plugins, interface_name, plugin["name"], relpath
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# If this is not of family list, just set a single entry for
|
|
686
|
+
# current plugin name.
|
|
687
|
+
# Since this is not a product plugin, we can ensure that these top-level
|
|
688
|
+
# attributes should exist. Don't include product_defaults or source_names in
|
|
689
|
+
# this info, because it doesn't apply to this type of plugin.
|
|
690
|
+
if "docstring" not in plugin:
|
|
691
|
+
error_message += f"""
|
|
692
|
+
Error with package '{plugin["package"]}':
|
|
693
|
+
docstring or family not defined in product
|
|
694
|
+
Must specify docstring and family in either product
|
|
695
|
+
or product_defaults.
|
|
696
|
+
interface '{plugin["interface"]}'
|
|
697
|
+
plugin name '{plugin["name"]}'
|
|
698
|
+
pkg relpath: '{relpath}'\n"""
|
|
699
|
+
|
|
700
|
+
else:
|
|
701
|
+
plugins[interface_name][plugin["name"]] = {
|
|
702
|
+
"docstring": format_docstring(plugin["docstring"]),
|
|
703
|
+
"family": plugin["family"],
|
|
704
|
+
"interface": plugin["interface"],
|
|
705
|
+
"package": package,
|
|
706
|
+
"plugin_type": "yaml_based",
|
|
707
|
+
"relpath": relpath,
|
|
708
|
+
}
|
|
709
|
+
return error_message
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def add_text_plugin(package, relpath, plugins):
|
|
713
|
+
"""Add all text plugins into plugin registries.
|
|
714
|
+
|
|
715
|
+
Parameters
|
|
716
|
+
----------
|
|
717
|
+
package: str
|
|
718
|
+
The current package being parsed
|
|
719
|
+
relpath: str
|
|
720
|
+
The relpath path to the text plugin
|
|
721
|
+
plugins: dict
|
|
722
|
+
A dictionary object of all installed package plugins
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
error_message: str
|
|
727
|
+
String containing informative error messages from any plugins that
|
|
728
|
+
were improperly formatted. An exception will be raised at the
|
|
729
|
+
very end if error_message is not the empty string - this allows
|
|
730
|
+
collecting ALL errors throughout the plugin registry process and
|
|
731
|
+
reporting them all at once, to facilitate rapidly identifying and
|
|
732
|
+
resolving errors.
|
|
733
|
+
"""
|
|
734
|
+
# Eventually, we will add interface, family, and name to the text files
|
|
735
|
+
# themselves, in which case we will pull the appropriate information out
|
|
736
|
+
# of the attributes included in the comments at the beginning of the text
|
|
737
|
+
# file. Note I added these comments to the current ascii_palettes, though
|
|
738
|
+
# they are not yet used.
|
|
739
|
+
|
|
740
|
+
# For now, use the basename of the filename as the "name"
|
|
741
|
+
text_name = splitext(basename(relpath))[0]
|
|
742
|
+
|
|
743
|
+
# For now, use the last directory name as the interface name.
|
|
744
|
+
interface_name = split(dirname(relpath))[-1]
|
|
745
|
+
error_message = ""
|
|
746
|
+
if interface_name not in plugins:
|
|
747
|
+
plugins[interface_name] = {}
|
|
748
|
+
plugins[interface_name][text_name] = {"package": package, "relpath": relpath}
|
|
749
|
+
# For now we have no error messages for text plugins, it will always be
|
|
750
|
+
# an empty string. But return it anyway.
|
|
751
|
+
return error_message
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def collect_module_plugin_metadata(
|
|
755
|
+
module, module_name, package, relpath, error_message
|
|
756
|
+
):
|
|
757
|
+
"""Collect metadata for the module-based plugin for the plugin registry.
|
|
758
|
+
|
|
759
|
+
Metadata necessary to collect here includes the interface of the plugin, the
|
|
760
|
+
plugin's name, its docstring, and the family it adheres to. Other metadata such as
|
|
761
|
+
relative path and the package it comes from is collected via 'add_class_plugin'.
|
|
762
|
+
|
|
763
|
+
NOTE: The 'Returns' section of this docstring is XOR. Either 'metadata' will be
|
|
764
|
+
returned, or 'error_message'. Not both or neither.
|
|
765
|
+
|
|
766
|
+
Parameters
|
|
767
|
+
----------
|
|
768
|
+
module: ModuleType
|
|
769
|
+
- The object of the module which has been loaded in realtime. This should have
|
|
770
|
+
top level attributes such as 'interface', 'name', 'family' and so on.
|
|
771
|
+
module_name: str
|
|
772
|
+
- The name of the .py file.
|
|
773
|
+
package: str
|
|
774
|
+
- The name of the package this plugin comes from
|
|
775
|
+
relpath: str
|
|
776
|
+
- The relative path to the plugin found in 'package'
|
|
777
|
+
error_message: str
|
|
778
|
+
- The current state of errors found during building the registry, stored as a
|
|
779
|
+
string.
|
|
780
|
+
|
|
781
|
+
Returns
|
|
782
|
+
-------
|
|
783
|
+
metadata: dict
|
|
784
|
+
- A dictionary of metadata for the provided plugin class to store in the plugin
|
|
785
|
+
registry.
|
|
786
|
+
error_message: str
|
|
787
|
+
- The current state of errors found during building the registry, stored as a
|
|
788
|
+
string.
|
|
789
|
+
"""
|
|
790
|
+
# Try to get "interface" variable from the module. This is required
|
|
791
|
+
# on ALL files within the python module based plugins directory, to
|
|
792
|
+
# ensure create_plugin_registries can explicitly tell whether a file
|
|
793
|
+
# is properly formatted or not. Files that are not full plugins must
|
|
794
|
+
# be specified with "interface = None" (identifying as a python module
|
|
795
|
+
# that should NOT be included in the python registry), and full
|
|
796
|
+
# plugins must include interface, family, and name variables at the top
|
|
797
|
+
# level.
|
|
798
|
+
try:
|
|
799
|
+
interface_name = module.interface
|
|
800
|
+
except AttributeError:
|
|
801
|
+
error_message += f"""\nError,
|
|
802
|
+
'interface' top level variable missing in
|
|
803
|
+
module '{module_name}' in
|
|
804
|
+
package '{package}'
|
|
805
|
+
at relpath '{relpath}'
|
|
806
|
+
|
|
807
|
+
* must specify 'interface' variable at the
|
|
808
|
+
top level of ALL python modules within the
|
|
809
|
+
plugins subdirectory.
|
|
810
|
+
|
|
811
|
+
* FOR VALID PLUGINS:
|
|
812
|
+
'interface', 'family', and 'name' must all be specified
|
|
813
|
+
as variables at the top level.
|
|
814
|
+
|
|
815
|
+
* FOR HELPER MODULES WITHIN THE plugins SUBDIRECTORY
|
|
816
|
+
'interface = None' must be specified at the top level for modules
|
|
817
|
+
within the plugins subdirectory that are not intended to be
|
|
818
|
+
plugins on their own."""
|
|
819
|
+
return error_message
|
|
820
|
+
# If interface is None, then legitimately skip the module.
|
|
821
|
+
# We want to skip this first, before we test anything else.
|
|
822
|
+
# If it is not a plugin, we don't care if there are other
|
|
823
|
+
# errors in it at this stage (ie, avoid unnecessary unrelated
|
|
824
|
+
# catastrophic failures)
|
|
825
|
+
if not interface_name:
|
|
826
|
+
LOG.info(
|
|
827
|
+
f"Skipping module '{module_name}' from '{package}', "
|
|
828
|
+
"interface_name is 'None'"
|
|
829
|
+
)
|
|
830
|
+
return error_message
|
|
831
|
+
# If we get here, it should be a full plugin, so it must include both
|
|
832
|
+
# name and family variables/attributes.
|
|
833
|
+
try:
|
|
834
|
+
name = module.name
|
|
835
|
+
family = module.family
|
|
836
|
+
except AttributeError:
|
|
837
|
+
error_message += f"""\nError, 'family' or 'name' top level variable missing
|
|
838
|
+
in module '{module_name}' in package '{package}'
|
|
839
|
+
at relpath '{relpath}'
|
|
840
|
+
must specify 'interface', 'family', and 'name' variables at the
|
|
841
|
+
top level of ALL module based plugins."""
|
|
842
|
+
return error_message
|
|
843
|
+
|
|
844
|
+
metadata = {
|
|
845
|
+
"docstring": format_docstring(module.__doc__),
|
|
846
|
+
"name": name,
|
|
847
|
+
"family": family,
|
|
848
|
+
"interface": interface_name,
|
|
849
|
+
"package": package,
|
|
850
|
+
"plugin_type": "class_based",
|
|
851
|
+
"signature": str(signature(module.call)),
|
|
852
|
+
"relpath": relpath,
|
|
853
|
+
"is_derived_from_module": True,
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return metadata
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def collect_class_plugin_metadata(plugin_class):
|
|
860
|
+
"""Collect metadata linked to the class plugin in 'module' for the plugin registry.
|
|
861
|
+
|
|
862
|
+
Metadata necessary to collect here includes the interface of the plugin, the
|
|
863
|
+
plugin's name, its docstring, and the family it adheres to. Other metadata such as
|
|
864
|
+
relative path and the package it comes from is collected via 'add_module_plugin'.
|
|
865
|
+
|
|
866
|
+
Parameters
|
|
867
|
+
----------
|
|
868
|
+
plugin_class: Object, default=None
|
|
869
|
+
- The plugin class from 'module' to collect metadata from. If None, this
|
|
870
|
+
function will attempt to locate the correct plugin class from the provided
|
|
871
|
+
module.
|
|
872
|
+
|
|
873
|
+
Returns
|
|
874
|
+
-------
|
|
875
|
+
metadata: dict
|
|
876
|
+
- A dictionary of metadata for the provided plugin class to store in the plugin
|
|
877
|
+
registry.
|
|
878
|
+
"""
|
|
879
|
+
# NOTE: Probably should add some attribute checks here as
|
|
880
|
+
# 'collect_module_plugin_metadata' does
|
|
881
|
+
metadata = {
|
|
882
|
+
"docstring": format_docstring(plugin_class.__doc__),
|
|
883
|
+
"name": plugin_class.name,
|
|
884
|
+
"family": plugin_class.family,
|
|
885
|
+
"interface": plugin_class.interface,
|
|
886
|
+
"plugin_type": "class_based",
|
|
887
|
+
"signature": str(signature(plugin_class.call)),
|
|
888
|
+
"is_derived_from_module": False,
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return metadata
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def add_class_plugin(package, relpath, plugins):
|
|
895
|
+
"""Add the class-based plugin associated with the filepaths and package to plugins.
|
|
896
|
+
|
|
897
|
+
NOTE: This function will work for 'legacy' module-based plugins that are dynamically
|
|
898
|
+
converted to class-based objects via <interface>.get_plugin and true class-based
|
|
899
|
+
plugins. Stores them all under the 'class_based' portion of the plugin registry and
|
|
900
|
+
denotes whether a plugin is truly class based or not via the metadata variable
|
|
901
|
+
'is_derived_from_module'.
|
|
902
|
+
|
|
903
|
+
Parameters
|
|
904
|
+
----------
|
|
905
|
+
package: str
|
|
906
|
+
The current package being parsed
|
|
907
|
+
relpath: str
|
|
908
|
+
The relpath path to the class-based plugin
|
|
909
|
+
plugins: dict
|
|
910
|
+
A dictionary object of all installed package plugins
|
|
911
|
+
|
|
912
|
+
Returns
|
|
913
|
+
-------
|
|
914
|
+
error_message: str
|
|
915
|
+
String containing informative error messages from any plugins that
|
|
916
|
+
were improperly formatted. An exception will be raised at the
|
|
917
|
+
very end if error_message is not the empty string - this allows
|
|
918
|
+
collecting ALL errors throughout the plugin registry process and
|
|
919
|
+
reporting them all at once, to facilitate rapidly identifying and
|
|
920
|
+
resolving errors.
|
|
921
|
+
"""
|
|
922
|
+
error_message = ""
|
|
923
|
+
|
|
924
|
+
if "__init__.py" in relpath:
|
|
925
|
+
# Ignore init files. We should probably ignore other dunder files such as
|
|
926
|
+
# __version.py, etc.
|
|
927
|
+
return error_message
|
|
928
|
+
|
|
929
|
+
module_name = splitext(basename(relpath))[0]
|
|
930
|
+
# We need the full path to the module in order
|
|
931
|
+
# for relative imports to work within modules.
|
|
932
|
+
module_path = splitext(relpath.replace("/", "."))[0]
|
|
933
|
+
module_path = f"{package}.{module_path}"
|
|
934
|
+
abspath = resources.files(package) / relpath
|
|
935
|
+
|
|
936
|
+
spec = util.spec_from_file_location(module_path, abspath)
|
|
937
|
+
module = util.module_from_spec(spec)
|
|
938
|
+
# Attempting importing module, catch ImportError
|
|
939
|
+
# We have to fix these to be able to import the module to
|
|
940
|
+
# see if 'interface' is defined, in order to see if it
|
|
941
|
+
# is a properly formatted python module.
|
|
942
|
+
try:
|
|
943
|
+
spec.loader.exec_module(module)
|
|
944
|
+
except ImportError as resp:
|
|
945
|
+
LOG.exception(resp)
|
|
946
|
+
error_message += f"""\nError {str(resp)}:
|
|
947
|
+
Failed importing '{module_name}' in
|
|
948
|
+
package '{package}'
|
|
949
|
+
at relpath '{relpath}'\n"""
|
|
950
|
+
return error_message
|
|
951
|
+
|
|
952
|
+
# We've encountered a truly 'class-based' plugin. I.e. we don't need to generate
|
|
953
|
+
# the plugin object from the module itself. Collect metadata from this plugin
|
|
954
|
+
# in a different fashion to what we've done previously.
|
|
955
|
+
|
|
956
|
+
# Once we've fully implemented class-based plugins and removed support for
|
|
957
|
+
# module-based plugins, we can remove this conditional and only keep the 'if'
|
|
958
|
+
# portion of the if-else statement below. The 'else' part of the conditional handles
|
|
959
|
+
# legacy module based plugins for the time being.
|
|
960
|
+
if "classes" in module_path or hasattr(module, "PLUGIN_CLASS"):
|
|
961
|
+
is_class = True
|
|
962
|
+
try:
|
|
963
|
+
PLUGIN_CLASS = getattr(module, "PLUGIN_CLASS")
|
|
964
|
+
except AttributeError:
|
|
965
|
+
plugin_class_error_message = (
|
|
966
|
+
f"Error: Detected class-based plugin at '{module_path}' but couldn't "
|
|
967
|
+
"locate the associated 'PLUGIN_CLASS' variable defining which class in "
|
|
968
|
+
"that module is the actual plugin class to use. \n Please define that "
|
|
969
|
+
"attribute before continuing. It should be a simple module-level "
|
|
970
|
+
"attribute that references the uninstantiated version of your plugin "
|
|
971
|
+
"class."
|
|
972
|
+
)
|
|
973
|
+
error_message += str(plugin_class_error_message)
|
|
974
|
+
return error_message
|
|
975
|
+
|
|
976
|
+
plugin_metadata = collect_class_plugin_metadata(PLUGIN_CLASS)
|
|
977
|
+
interface_name = plugin_metadata["interface"]
|
|
978
|
+
name = plugin_metadata.pop("name")
|
|
979
|
+
|
|
980
|
+
plugin_metadata["package"] = package
|
|
981
|
+
plugin_metadata["relpath"] = relpath
|
|
982
|
+
else:
|
|
983
|
+
is_class = False
|
|
984
|
+
# Collect top level metadata from the module plugin
|
|
985
|
+
return_type = collect_module_plugin_metadata(
|
|
986
|
+
module, module_name, package, relpath, error_message
|
|
987
|
+
)
|
|
988
|
+
if isinstance(return_type, str):
|
|
989
|
+
# This conditional checks if the returned object is of a string instance. If
|
|
990
|
+
# it is, that means an error message was added and we should just return it
|
|
991
|
+
return return_type
|
|
992
|
+
else:
|
|
993
|
+
# Otherwise, metadata was returned as expected
|
|
994
|
+
plugin_metadata = return_type
|
|
995
|
+
name = plugin_metadata.pop("name")
|
|
996
|
+
interface_name = plugin_metadata["interface"]
|
|
997
|
+
|
|
998
|
+
# If the current interface_name is not in the plugins dictionary yet, add it
|
|
999
|
+
# as an empty dictionary.
|
|
1000
|
+
if interface_name not in plugins.keys():
|
|
1001
|
+
plugins[interface_name] = {}
|
|
1002
|
+
# Check_plugin_exists will return a text error message if there are any errors
|
|
1003
|
+
# rather than raising an exception. This allows collecting all errors as
|
|
1004
|
+
# we go, and reporting once at the end with an error message including ALL
|
|
1005
|
+
# errors found across all plugins in all plugin packages. Append the new
|
|
1006
|
+
# error message to the error messages that have already been collected.
|
|
1007
|
+
error_message += check_plugin_exists(
|
|
1008
|
+
package, plugins, interface_name, name, relpath
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
# Add the generated metadata to the plugin's entry in the registry
|
|
1012
|
+
plugins[interface_name][name] = plugin_metadata
|
|
1013
|
+
|
|
1014
|
+
if interface_name == "readers":
|
|
1015
|
+
if not is_class and hasattr(module, "source_names"):
|
|
1016
|
+
plugins[interface_name][name]["source_names"] = module.source_names
|
|
1017
|
+
elif is_class and hasattr(PLUGIN_CLASS, "source_names"):
|
|
1018
|
+
plugins[interface_name][name]["source_names"] = PLUGIN_CLASS.source_names
|
|
1019
|
+
else:
|
|
1020
|
+
warnings.warn(
|
|
1021
|
+
(
|
|
1022
|
+
f"Plugin package '{package}'s reader"
|
|
1023
|
+
f" plugin '{name}' is using a deprecated source_names "
|
|
1024
|
+
"implementation. Please add a module/class-level 'source_names' "
|
|
1025
|
+
"attribute to this plugin and re-run "
|
|
1026
|
+
"'pluginify create'."
|
|
1027
|
+
),
|
|
1028
|
+
DeprecationWarning,
|
|
1029
|
+
stacklevel=2,
|
|
1030
|
+
)
|
|
1031
|
+
plugins[interface_name][name]["source_names"] = ["Unspecified"]
|
|
1032
|
+
|
|
1033
|
+
del module
|
|
1034
|
+
# Return the final error message - an exception will be raised at the very
|
|
1035
|
+
# end after collecting and reporting on all errors if there were any errors
|
|
1036
|
+
# during plugin registry creation.
|
|
1037
|
+
return error_message
|