pluginify 0.0.0.post1.dev0__tar.gz → 0.1.0__tar.gz

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.
Files changed (42) hide show
  1. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/.github/workflows/package-and-publish.yaml +2 -0
  2. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/PKG-INFO +6 -4
  3. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/README.md +2 -2
  4. pluginify-0.1.0/docs/source/releases/latest/pluginify-updates-jeremy-comments.yaml +27 -0
  5. pluginify-0.1.0/docs/source/releases/latest/pypi-publish-bug-fix.yaml +15 -0
  6. pluginify-0.1.0/docs/source/releases/latest/version-update.yaml +15 -0
  7. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/_version.py +2 -2
  8. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/commandline_typer.py +94 -10
  9. pluginify-0.1.0/pluginify/utils/validators.py +183 -0
  10. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pyproject.toml +2 -2
  11. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/unit_tests/plugin_registries/test_plugin_registries.py +7 -175
  12. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/.github/workflows/generate-badges.yaml +0 -0
  13. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/.github/workflows/lint-and-test.yaml +0 -0
  14. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/.github/workflows/status-check.yaml +0 -0
  15. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/LICENSE +0 -0
  16. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/docs/source/releases/latest/ci-fix-perms.yaml +0 -0
  17. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/docs/source/releases/latest/copy-geoips-functionality.yaml +0 -0
  18. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/docs/source/releases/latest/initial-commit.yaml +0 -0
  19. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/docs/source/releases/latest/remove-all-geoips-references.yaml +0 -0
  20. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/docs/source/releases/latest/replace-geoips-functionality.yaml +0 -0
  21. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/docs/source/releases/latest/update-black-formatting.yaml +0 -0
  22. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/__init__.py +0 -0
  23. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/config.py +0 -0
  24. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/create_plugin_registries.py +0 -0
  25. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/errors.py +0 -0
  26. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/interfaces/__init__.py +0 -0
  27. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/interfaces/base.py +0 -0
  28. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/interfaces/class_based/data_modifiers.py +0 -0
  29. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/interfaces/class_based_plugin.py +0 -0
  30. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/interfaces/yaml_based/configs.py +0 -0
  31. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/plugin_registry.py +0 -0
  32. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/plugins/classes/data_modifiers/cuboid.py +0 -0
  33. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/plugins/yaml/configs/stucco.yaml +0 -0
  34. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/pydantic_models/v1/configs.py +0 -0
  35. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/utils/__init__.py +0 -0
  36. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/pluginify/utils/context_managers.py +0 -0
  37. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/conftest.py +0 -0
  38. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/unit_tests/config/test_config.py +0 -0
  39. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/unit_tests/plugin_registries/files/bad/invalid_interfaces.yaml +0 -0
  40. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/unit_tests/plugin_registries/files/bad/missing_lowest_keys.yaml +0 -0
  41. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/unit_tests/plugin_registries/files/bad/missing_plugin_types.yaml +0 -0
  42. {pluginify-0.0.0.post1.dev0 → pluginify-0.1.0}/tests/unit_tests/plugin_registries/files/good/pluginify.yaml +0 -0
@@ -26,6 +26,8 @@ jobs:
26
26
  runs-on: ubuntu-latest
27
27
  steps:
28
28
  - uses: actions/checkout@v4
29
+ with:
30
+ fetch-depth: 0 # make poetry dynamic versioning actually see our git tags
29
31
 
30
32
  - uses: actions/setup-python@v5
31
33
  with:
@@ -1,15 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pluginify
3
- Version: 0.0.0.post1.dev0
3
+ Version: 0.1.0
4
4
  Summary: Pluginify package
5
5
  License: LICENSE
6
6
  License-File: LICENSE
7
7
  Author: Evan Rose
8
- Requires-Python: >=3.11.0,<3.13.0
8
+ Requires-Python: >=3.11.0
9
9
  Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Provides-Extra: doc
14
16
  Requires-Dist: docstring_parser
15
17
  Requires-Dist: jsonschema (>4.18.0)
@@ -49,9 +51,9 @@ git clone https://github.com/NRLMMD-GEOIPS/pluginify.git
49
51
  # cd to pluginify's top level dir
50
52
  pip install -e .
51
53
  ```
52
- In the future:
54
+ OR
53
55
  ```bash
54
- pip install pluginify
56
+ pip install pluginify
55
57
  ```
56
58
 
57
59
  Use pluginify
@@ -25,9 +25,9 @@ git clone https://github.com/NRLMMD-GEOIPS/pluginify.git
25
25
  # cd to pluginify's top level dir
26
26
  pip install -e .
27
27
  ```
28
- In the future:
28
+ OR
29
29
  ```bash
30
- pip install pluginify
30
+ pip install pluginify
31
31
  ```
32
32
 
33
33
  Use pluginify
@@ -0,0 +1,27 @@
1
+ documentation:
2
+ - title: 'Minor updates to pluginify CLI and unit tests'
3
+ description: |
4
+ Updates to CLI help information and refactoring of plugin registry unit test code.
5
+
6
+ Moved PluginRegistryValidator class to a utility module so packages outside of
7
+ pluginify could make use of that code.
8
+
9
+ Updating CLI commands help information by providing examples and more context.
10
+
11
+ Removed <3.13 restriction for python.
12
+ files:
13
+ added:
14
+ - pluginify/utils/validators.py
15
+ modified:
16
+ - docs/source/concepts/command_line/index.rst
17
+ - docs/source/concepts/functionality/index.rst
18
+ - pluginify/commandline_typer.py
19
+ - tests/unit_tests/plugin_registries/test_plugin_registries.py
20
+ - README.md
21
+ - pyproject.toml
22
+ related-issue:
23
+ number: null
24
+ repo_url: ''
25
+ date:
26
+ start: null
27
+ finish: null
@@ -0,0 +1,15 @@
1
+ continuous integration:
2
+ - title: 'PyPi Publish bug fix'
3
+ description: |
4
+ Updated package-and-publish.yaml workflow to recognize dynamic versioning via git
5
+ tags. Hopefully this will work.
6
+ files:
7
+ modified:
8
+ - .github/workflows/package-and-publish.yaml
9
+ - pyproject.toml
10
+ related-issue:
11
+ number: null
12
+ repo_url: ''
13
+ date:
14
+ start: null
15
+ finish: null
@@ -0,0 +1,15 @@
1
+ continuous integration:
2
+ - title: 'Pluginify Version Update'
3
+ description: |
4
+ Updated package version from v0.0.0 to v1.0.0. Needed to push most up to date code
5
+ to PyPi.
6
+ files:
7
+ modified:
8
+ - pluginify/_version.py
9
+ - pyproject.toml
10
+ related-issue:
11
+ number: null
12
+ repo_url: ''
13
+ date:
14
+ start: null
15
+ finish: null
@@ -3,5 +3,5 @@
3
3
 
4
4
  # DO NOT EDIT
5
5
  # managed by poetry-dynamic-versioning
6
- __version__ = "0.0.0.post1.dev0"
7
- __version_tuple__ = (0, 0, 0, "post1", "dev0")
6
+ __version__ = "0.1.0"
7
+ __version_tuple__ = (0, 1, 0)
@@ -56,8 +56,8 @@ def configure_logging(level=logging.INFO):
56
56
  root = logging.getLogger()
57
57
 
58
58
  # Avoid duplicate handlers if CLI is called multiple times
59
- if root.handlers:
60
- return
59
+ # if root.handlers:
60
+ # return
61
61
 
62
62
  root.setLevel(level)
63
63
 
@@ -131,6 +131,10 @@ class DocstringTyper(typer.Typer):
131
131
  if doc.long_description:
132
132
  new_doc += "\n\n" + doc.long_description
133
133
 
134
+ # Keep examples portion in command's help output if specified
135
+ if doc.examples:
136
+ new_doc += "\n\nExamples\n--------\n" + doc.examples[0].description
137
+
134
138
  func.__doc__ = new_doc
135
139
 
136
140
  new_params = []
@@ -165,10 +169,32 @@ class DocstringTyper(typer.Typer):
165
169
  return wrapper
166
170
 
167
171
 
168
- app = DocstringTyper(context_settings={"help_option_names": ["-h", "--help"]})
172
+ app = DocstringTyper(
173
+ context_settings={"help_option_names": ["-h", "--help"]},
174
+ help="pluginify command line interface.",
175
+ )
169
176
  config_app = DocstringTyper()
170
-
171
- app.add_typer(config_app, name="config", help="Configuration commands for pluginify.")
177
+ config_set_app = DocstringTyper()
178
+
179
+ config_variable_help = (
180
+ "NAMESPACE specifies the default namespace in which pluginify will manage "
181
+ "registries.\n"
182
+ "REGISTRY_DIRECTORY specifies the base directory where registries and their "
183
+ "supporting directory structure will be written.\n"
184
+ "REBUILD_REGISTRIES informs pluginify whether or not to rebuild registries at "
185
+ "runtime if a plugin cannot be located."
186
+ )
187
+
188
+ app.add_typer(
189
+ config_app,
190
+ name="config",
191
+ help=f"Configure pluginify.\n\n{config_variable_help}",
192
+ )
193
+ config_app.add_typer(
194
+ config_set_app,
195
+ name="set",
196
+ help=f"Set pluginify configuration variables.\n\n{config_variable_help}",
197
+ )
172
198
 
173
199
 
174
200
  @app.command()
@@ -177,7 +203,37 @@ def create(
177
203
  packages: Optional[List[str]] = None,
178
204
  save_type: Literal["json", "yaml"] = "json",
179
205
  ):
180
- """Create plugin registry files for one or more packages under a given namespace.
206
+ """Create plugin registries.
207
+
208
+ Plugin registries are low-level files which contain dictionaries of metadata for all
209
+ plugins implemented in one or more packages registered under a common namespace.
210
+
211
+ Pluginify creates and makes use of these registry files to properly load and
212
+ instantiate your plugins.
213
+
214
+ Examples
215
+ --------
216
+ Namespace: packageX.plugin_packages
217
+ Directory structure:
218
+
219
+ packageX/
220
+ plugins/
221
+ interfaces/
222
+ utils/
223
+ registered_plugins.json [will create]
224
+ packageY/
225
+ plugins/
226
+ registered_plugins.json [will create]
227
+
228
+ JSON registry files are faster to load and are used by default. YAML
229
+ extensions are also supported for easier viewing.
230
+
231
+ CLI examples
232
+ ------------
233
+ pluginify create
234
+ pluginify create -n packageX.plugin_packages
235
+ pluginify create -s yaml
236
+ pluginify create -n packageX.plugin_packages -p packageY
181
237
 
182
238
  Parameters
183
239
  ----------
@@ -199,7 +255,35 @@ def delete(
199
255
  namespace: str = NAMESPACE,
200
256
  packages: Optional[List[str]] = None,
201
257
  ):
202
- """Delete plugin registry files for one or more packages under a given namespace.
258
+ """Delete plugin registries.
259
+
260
+ Plugin registries are low-level files which contain dictionaries of metadata for all
261
+ plugins implemented in one or more packages registered under a common namespace.
262
+
263
+ By default, this command will delete every .json and .yaml instance of registry
264
+ files for every package registered under a given namespace. If you only want to
265
+ delete registry files from a subset of packages, use the '-p' flag to specify those
266
+ packages.
267
+
268
+ Examples
269
+ --------
270
+ Namespace: packageX.plugin_packages
271
+ Directory structure:
272
+
273
+ packageX/
274
+ plugins/
275
+ interfaces/
276
+ utils/
277
+ registered_plugins.json [will delete]
278
+ packageY/
279
+ plugins/
280
+ registered_plugins.json [will delete]
281
+
282
+ CLI examples
283
+ ------------
284
+ pluginify delete
285
+ pluginify delete -n packageX.plugin_packages
286
+ pluginify delete -n packageX.plugin_packages -p packageY
203
287
 
204
288
  Parameters
205
289
  ----------
@@ -213,7 +297,7 @@ def delete(
213
297
  plugin_registry.delete_registries(packages=packages)
214
298
 
215
299
 
216
- @config_app.command("set-rebuild-registries")
300
+ @config_set_app.command("rebuild-registries")
217
301
  def set_rebuild_registries(rebuild_registries: bool):
218
302
  """Set pluginify's REBUILD_REGISTRIES config variable.
219
303
 
@@ -226,7 +310,7 @@ def set_rebuild_registries(rebuild_registries: bool):
226
310
  update_existing_fields({"REBUILD_REGISTRIES": rebuild_registries})
227
311
 
228
312
 
229
- @config_app.command("set-namespace")
313
+ @config_set_app.command("namespace")
230
314
  def set_namespace(namespace: str):
231
315
  """Set pluginify's NAMESPACE config variable.
232
316
 
@@ -239,7 +323,7 @@ def set_namespace(namespace: str):
239
323
  update_existing_fields({"NAMESPACE": namespace})
240
324
 
241
325
 
242
- @config_app.command("set-registry-directory")
326
+ @config_set_app.command("registry-directory")
243
327
  def set_registry_directory(registry_directory: Path):
244
328
  """Set pluginify's REGISTRY_DIRECTORY config variable.
245
329
 
@@ -0,0 +1,183 @@
1
+ """Pluginify validators module.
2
+
3
+ Currently only implements a PluginRegistryValidator class.
4
+ """
5
+
6
+ from importlib import metadata, import_module
7
+ import json
8
+ from pprint import pformat
9
+
10
+ import pytest
11
+
12
+ from pluginify.errors import PluginRegistryError
13
+ from pluginify.plugin_registry import PluginRegistry
14
+
15
+
16
+ class PluginRegistryValidator(PluginRegistry):
17
+ """Subclass of PluginRegistry which adds functionality for unit testing."""
18
+
19
+ def __init__(self, namespace, fpaths=None):
20
+ """Initialize TestPluginRegistry Class."""
21
+ self.namespace = namespace
22
+ super().__init__(self.namespace, _test_registry_files=fpaths)
23
+
24
+ def validate_plugin_types_exist(self, reg_dict, reg_path):
25
+ """Test that all top level plugin types exist in each registry file."""
26
+ expected_plugin_types = ["yaml_based", "class_based", "text_based"]
27
+ for p_type in expected_plugin_types:
28
+ if p_type not in reg_dict:
29
+ error_str = f"Expected plugin type '{p_type}' to be in the registry but"
30
+ error_str += f" wasn't. This was in file '{reg_path}'."
31
+ raise PluginRegistryError(error_str)
32
+
33
+ def validate_all_registries(self):
34
+ """Validate all registries in the current installation.
35
+
36
+ This should be run during testing, but not at runtime.
37
+ Ensure we do not fail catastrophically for a single bad plugin
38
+ at runtime, so test up front to test validity.
39
+ """
40
+ for reg_path in self.registry_files:
41
+ pkg_plugins = json.load(open(reg_path, "r"))
42
+ self.validate_registry(pkg_plugins, reg_path)
43
+
44
+ def validate_registry(self, current_registry, fpath):
45
+ """Test all plugins found in registered plugins for their validity."""
46
+ try:
47
+ self.validate_plugin_types_exist(current_registry, fpath)
48
+ except PluginRegistryError as e:
49
+ # xfail if this is a test, otherwise just raise PluginRegistryError
50
+ if self._is_test:
51
+ pytest.xfail(str(e))
52
+ else:
53
+ raise PluginRegistryError(e)
54
+ try:
55
+ self.validate_registry_interfaces(current_registry)
56
+ except PluginRegistryError as e:
57
+ # xfail if this is a test, otherwise just raise PluginRegistryError
58
+ if self._is_test:
59
+ pytest.xfail(str(e))
60
+ else:
61
+ raise PluginRegistryError(e)
62
+ for plugin_type in current_registry:
63
+ for interface in current_registry[plugin_type]:
64
+ for plugin in current_registry[plugin_type][interface]:
65
+ try:
66
+ if interface == "products":
67
+ for subplg in current_registry[plugin_type][interface][
68
+ plugin
69
+ ]:
70
+ self.validate_plugin_attrs(
71
+ plugin_type,
72
+ interface,
73
+ (plugin, subplg),
74
+ current_registry[plugin_type][interface][plugin][
75
+ subplg
76
+ ],
77
+ )
78
+ elif plugin_type != "text_based":
79
+ self.validate_plugin_attrs(
80
+ plugin_type,
81
+ interface,
82
+ plugin,
83
+ current_registry[plugin_type][interface][plugin],
84
+ )
85
+ except PluginRegistryError as e:
86
+ # xfail if this is a test,
87
+ # otherwise just raise PluginRegistryError
88
+ if self._is_test:
89
+ pytest.xfail(str(e))
90
+ else:
91
+ raise PluginRegistryError(e)
92
+
93
+ def validate_plugin_attrs(self, plugin_type, interface, name, plugin):
94
+ """Test non-product plugin for all required attributes."""
95
+ missing = []
96
+ if plugin_type == "yaml_based" and interface != "products":
97
+ attrs = [
98
+ "docstring",
99
+ "family",
100
+ "interface",
101
+ "package",
102
+ "plugin_type",
103
+ "relpath",
104
+ ]
105
+ elif plugin_type == "yaml_based":
106
+ attrs = [
107
+ "docstring",
108
+ "family",
109
+ "interface",
110
+ "package",
111
+ "plugin_type",
112
+ "product_defaults",
113
+ "source_names",
114
+ "relpath",
115
+ ]
116
+ else:
117
+ attrs = [
118
+ "docstring",
119
+ "family",
120
+ "interface",
121
+ "package",
122
+ "plugin_type",
123
+ "signature",
124
+ "relpath",
125
+ ]
126
+ for attr in attrs:
127
+ try:
128
+ plugin[attr]
129
+ except KeyError:
130
+ missing.append(attr)
131
+ if missing:
132
+ raise PluginRegistryError(
133
+ f"Plugin '{name}' is missing the following required "
134
+ f"top-level properties: '{missing}'"
135
+ )
136
+
137
+ def validate_registry_interfaces(self, current_registry):
138
+ """Test Plugin Registry interfaces validity."""
139
+ yaml_interfaces = []
140
+ class_interfaces = []
141
+
142
+ # NOTE: all <pkg>/interfaces/__init__.py files MUST
143
+ # contain a "class_based_interfaces" list and a
144
+ # "yaml_based_interfaces" list - this is how we
145
+ # identify the valid interface names.
146
+ # We must avoid actually importing the interfaces within
147
+ # the pluginify repo - or we will end up with a circular import
148
+ # due to BaseInterface.
149
+ for pkg in metadata.entry_points(group=self.namespace):
150
+ try:
151
+ mod = import_module(f"{pkg.value}.interfaces")
152
+ except ModuleNotFoundError as resp:
153
+ if f"No module named '{pkg.value}.interfaces'" in str(resp):
154
+ continue
155
+ else:
156
+ raise ModuleNotFoundError(resp)
157
+ class_interfaces += mod.class_based_interfaces
158
+ yaml_interfaces += mod.yaml_based_interfaces
159
+
160
+ bad_interfaces = []
161
+ for plugin_type in ["class_based", "yaml_based"]:
162
+ for interface in current_registry[plugin_type]:
163
+ if (
164
+ interface not in class_interfaces
165
+ and interface not in yaml_interfaces
166
+ ):
167
+ error_str = f"\nPlugin type '{plugin_type}' does not allow "
168
+ error_str += f"interface '{interface}'.\n"
169
+ error_str += "\nValid interfaces: "
170
+ if plugin_type == "class_based":
171
+ interface_list = class_interfaces
172
+ else:
173
+ interface_list = yaml_interfaces
174
+ error_str += f"\n{interface_list}\n"
175
+ error_str += "\nPlease update the following plugins "
176
+ error_str += "to use a valid interface:\n"
177
+ error_str += pformat(current_registry[plugin_type][interface])
178
+ bad_interfaces.append(error_str)
179
+ if bad_interfaces:
180
+ error_str = "The following interfaces were not valid:\n"
181
+ for error in bad_interfaces:
182
+ error_str += error
183
+ raise PluginRegistryError(error_str)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pluginify" # package available at the root of the project
3
- version = "0.0.0.post1.dev0"
3
+ version = "0.1.0"
4
4
  description = "Pluginify package"
5
5
  authors = ["Evan Rose", "evrose54@colostate.edu"]
6
6
  readme = "README.md"
@@ -38,7 +38,7 @@ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
38
38
  build-backend = "poetry_dynamic_versioning.backend" # object used to perform the build process
39
39
 
40
40
  [tool.poetry.dependencies] # Include any python dependencies here
41
- python = ">=3.11.0,<3.13.0" # mandatory to declare the required python version
41
+ python = ">=3.11.0" # mandatory to declare the required python version
42
42
  pydantic = ">=2.10.0" # Data validation using Python type hints
43
43
  pyyaml = "*" # Base requirement
44
44
  jsonschema = ">4.18.0" # Provides validation for YAML plugins
@@ -4,19 +4,16 @@
4
4
  """TestPluginRegistry Class used for Unit Testing the Plugin Registries."""
5
5
 
6
6
  from glob import glob
7
- from importlib import import_module, metadata
8
7
  import logging
9
8
  from os.path import basename, splitext
10
- from pprint import pformat
11
9
 
12
10
  import pytest
13
- import json
14
11
  import yaml
15
12
 
16
13
  from pluginify.errors import PluginError, PluginRegistryError
17
14
  from pluginify.interfaces import configs
18
15
  from pluginify.interfaces import data_modifiers
19
- from pluginify.plugin_registry import PluginRegistry
16
+ from pluginify.utils.validators import PluginRegistryValidator
20
17
 
21
18
  LOG = logging.getLogger(__name__)
22
19
 
@@ -28,175 +25,6 @@ class FakeInterface:
28
25
  name = "fake_interface"
29
26
 
30
27
 
31
- class PluginRegistryValidator(PluginRegistry):
32
- """Subclass of PluginRegistry which adds functionality for unit testing."""
33
-
34
- def __init__(self, fpaths=None):
35
- """Initialize TestPluginRegistry Class."""
36
- super().__init__("pluginify.plugin_packages", _test_registry_files=fpaths)
37
-
38
- def validate_plugin_types_exist(self, reg_dict, reg_path):
39
- """Test that all top level plugin types exist in each registry file."""
40
- expected_plugin_types = ["yaml_based", "class_based", "text_based"]
41
- for p_type in expected_plugin_types:
42
- if p_type not in reg_dict:
43
- error_str = f"Expected plugin type '{p_type}' to be in the registry but"
44
- error_str += f" wasn't. This was in file '{reg_path}'."
45
- raise PluginRegistryError(error_str)
46
-
47
- def validate_all_registries(self):
48
- """Validate all registries in the current installation.
49
-
50
- This should be run during testing, but not at runtime.
51
- Ensure we do not fail catastrophically for a single bad plugin
52
- at runtime, so test up front to test validity.
53
- """
54
- for reg_path in self.registry_files:
55
- pkg_plugins = json.load(open(reg_path, "r"))
56
- self.validate_registry(pkg_plugins, reg_path)
57
-
58
- def validate_registry(self, current_registry, fpath):
59
- """Test all plugins found in registered plugins for their validity."""
60
- try:
61
- self.validate_plugin_types_exist(current_registry, fpath)
62
- except PluginRegistryError as e:
63
- # xfail if this is a test, otherwise just raise PluginRegistryError
64
- if self._is_test:
65
- pytest.xfail(str(e))
66
- else:
67
- raise PluginRegistryError(e)
68
- try:
69
- self.validate_registry_interfaces(current_registry)
70
- except PluginRegistryError as e:
71
- # xfail if this is a test, otherwise just raise PluginRegistryError
72
- if self._is_test:
73
- pytest.xfail(str(e))
74
- else:
75
- raise PluginRegistryError(e)
76
- for plugin_type in current_registry:
77
- for interface in current_registry[plugin_type]:
78
- for plugin in current_registry[plugin_type][interface]:
79
- try:
80
- if interface == "products":
81
- for subplg in current_registry[plugin_type][interface][
82
- plugin
83
- ]:
84
- self.validate_plugin_attrs(
85
- plugin_type,
86
- interface,
87
- (plugin, subplg),
88
- current_registry[plugin_type][interface][plugin][
89
- subplg
90
- ],
91
- )
92
- elif plugin_type != "text_based":
93
- self.validate_plugin_attrs(
94
- plugin_type,
95
- interface,
96
- plugin,
97
- current_registry[plugin_type][interface][plugin],
98
- )
99
- except PluginRegistryError as e:
100
- # xfail if this is a test,
101
- # otherwise just raise PluginRegistryError
102
- if self._is_test:
103
- pytest.xfail(str(e))
104
- else:
105
- raise PluginRegistryError(e)
106
-
107
- def validate_plugin_attrs(self, plugin_type, interface, name, plugin):
108
- """Test non-product plugin for all required attributes."""
109
- missing = []
110
- if plugin_type == "yaml_based" and interface != "products":
111
- attrs = [
112
- "docstring",
113
- "family",
114
- "interface",
115
- "package",
116
- "plugin_type",
117
- "relpath",
118
- ]
119
- elif plugin_type == "yaml_based":
120
- attrs = [
121
- "docstring",
122
- "family",
123
- "interface",
124
- "package",
125
- "plugin_type",
126
- "product_defaults",
127
- "source_names",
128
- "relpath",
129
- ]
130
- else:
131
- attrs = [
132
- "docstring",
133
- "family",
134
- "interface",
135
- "package",
136
- "plugin_type",
137
- "signature",
138
- "relpath",
139
- ]
140
- for attr in attrs:
141
- try:
142
- plugin[attr]
143
- except KeyError:
144
- missing.append(attr)
145
- if missing:
146
- raise PluginRegistryError(
147
- f"Plugin '{name}' is missing the following required "
148
- f"top-level properties: '{missing}'"
149
- )
150
-
151
- def validate_registry_interfaces(self, current_registry):
152
- """Test Plugin Registry interfaces validity."""
153
- yaml_interfaces = []
154
- class_interfaces = []
155
-
156
- # NOTE: all <pkg>/interfaces/__init__.py files MUST
157
- # contain a "class_based_interfaces" list and a
158
- # "yaml_based_interfaces" list - this is how we
159
- # identify the valid interface names.
160
- # We must avoid actually importing the interfaces within
161
- # the pluginify repo - or we will end up with a circular import
162
- # due to BaseInterface.
163
- for pkg in metadata.entry_points(group="pluginify.plugin_packages"):
164
- try:
165
- mod = import_module(f"{pkg.value}.interfaces")
166
- except ModuleNotFoundError as resp:
167
- if f"No module named '{pkg.value}.interfaces'" in str(resp):
168
- continue
169
- else:
170
- raise ModuleNotFoundError(resp)
171
- class_interfaces += mod.class_based_interfaces
172
- yaml_interfaces += mod.yaml_based_interfaces
173
-
174
- bad_interfaces = []
175
- for plugin_type in ["class_based", "yaml_based"]:
176
- for interface in current_registry[plugin_type]:
177
- if (
178
- interface not in class_interfaces
179
- and interface not in yaml_interfaces
180
- ):
181
- error_str = f"\nPlugin type '{plugin_type}' does not allow "
182
- error_str += f"interface '{interface}'.\n"
183
- error_str += "\nValid interfaces: "
184
- if plugin_type == "class_based":
185
- interface_list = class_interfaces
186
- else:
187
- interface_list = yaml_interfaces
188
- error_str += f"\n{interface_list}\n"
189
- error_str += "\nPlease update the following plugins "
190
- error_str += "to use a valid interface:\n"
191
- error_str += pformat(current_registry[plugin_type][interface])
192
- bad_interfaces.append(error_str)
193
- if bad_interfaces:
194
- error_str = "The following interfaces were not valid:\n"
195
- for error in bad_interfaces:
196
- error_str += error
197
- raise PluginRegistryError(error_str)
198
-
199
-
200
28
  class TestPluginRegistry:
201
29
  """
202
30
  Pytest-based Unit Test for the PluginRegistry Class.
@@ -212,12 +40,16 @@ class TestPluginRegistry:
212
40
  )
213
41
 
214
42
  # pr_validator uses test registry files for most tests
215
- pr_validator = PluginRegistryValidator(fpaths=default_fpaths)
43
+ pr_validator = PluginRegistryValidator(
44
+ namespace="pluginify.plugin_packages", fpaths=default_fpaths
45
+ )
216
46
  # real reg validator is responsible for testing deleting and building of registries
217
47
  # based on whether or not we want that to occur:
218
48
  # I.e. whether or not config.REBUILD_REGISTRIES is set to True or
219
49
  # False
220
- real_reg_validator = PluginRegistryValidator(fpaths=None)
50
+ real_reg_validator = PluginRegistryValidator(
51
+ namespace="pluginify.plugin_packages", fpaths=None
52
+ )
221
53
 
222
54
  # Couldn't implement this class via inheritance because PyTest Raised this error:
223
55
  # PytestCollectionWarning: cannot collect test class 'TestPluginRegistry' because
File without changes