pluginify 1.0.0.post16.dev0__py3-none-any.whl → 1.0.0.post41.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.
@@ -31,6 +31,7 @@ from pathlib import Path
31
31
  import re
32
32
  import tempfile
33
33
  import warnings
34
+ from typing import Any
34
35
 
35
36
  import yaml
36
37
 
@@ -40,7 +41,7 @@ from pluginify.errors import PluginRegistryError
40
41
  LOG = logging.getLogger(__name__)
41
42
 
42
43
 
43
- def format_docstring(docstring, use_regex=True):
44
+ def format_docstring(docstring: str, use_regex: bool = True) -> str:
44
45
  """Format the provided docstring placement in the plugin registry.
45
46
 
46
47
  Found when using the CLI and inspecting the registry, some docstrings are formatted
@@ -50,12 +51,12 @@ def format_docstring(docstring, use_regex=True):
50
51
 
51
52
  Parameters
52
53
  ----------
53
- docstring: str
54
- - The docstring which we are going to format.
55
- use_regex: bool, optional (default=False)
56
- - Whether or not we want to apply regex formatting to the docstring. Usually
57
- recommended as it will replace 'newline' chars but not purposeful
58
- '.newline' strings.
54
+ docstring : str
55
+ The docstring which we are going to format.
56
+ use_regex : bool, optional (default=False)
57
+ Whether or not we want to apply regex formatting to the docstring. Usually
58
+ recommended as it will replace 'newline' chars but not purposeful
59
+ '.newline' strings.
59
60
  """
60
61
  if docstring:
61
62
  if use_regex:
@@ -71,23 +72,19 @@ def format_docstring(docstring, use_regex=True):
71
72
  return docstring
72
73
 
73
74
 
74
- def remove_registries(namespace, plugin_packages):
75
+ def remove_registries(namespace: str, plugin_packages: list) -> None:
75
76
  """Remove all plugin registries if a PluginRegistryError is raised.
76
77
 
77
78
  Parameters
78
79
  ----------
79
- namespace: str
80
+ namespace : str
80
81
  Namespace that your plugin packages fall under. The argument parser defaults
81
82
  this value to 'pluginify.plugin_packages', but a user can create separate
82
83
  namespaces if developing interfaces outside of pluginify.
83
- plugin_packages: list[EntryPoints]
84
+ plugin_packages : list[EntryPoints]
84
85
  A list of EntryPoints pointing to each installed package --> ie.
85
86
  [EntryPoint(name='pluginify', value='pluginify',
86
87
  group='pluginify.plugin_packages'), ...]
87
-
88
- Returns
89
- -------
90
- None
91
88
  """
92
89
  # Remove registries is called whenever an improperly formatted plugin or
93
90
  # package is encountered. This is not called until all errors have been
@@ -111,7 +108,9 @@ def remove_registries(namespace, plugin_packages):
111
108
  remove(json_plug_path)
112
109
 
113
110
 
114
- def registry_sanity_check(plugin_packages, save_type, namespace):
111
+ def registry_sanity_check(
112
+ plugin_packages: list, save_type: str, namespace: str
113
+ ) -> None:
115
114
  """Check that each plugin package registry has no duplicate lowest depth entries.
116
115
 
117
116
  If it does, raise a `PluginRegistryError` for that specific package, then remove all
@@ -121,19 +120,15 @@ def registry_sanity_check(plugin_packages, save_type, namespace):
121
120
 
122
121
  Parameters
123
122
  ----------
124
- plugin_packages: list EntryPoints
123
+ plugin_packages : list EntryPoints
125
124
  A list of EntryPoints pointing to each installed package --> ie.
126
125
  `[EntryPoint(name='pluginify', value='pluginify',
127
126
  group='pluginify.plugin_packages')]`
128
- save_type: str
127
+ save_type : str
129
128
  The file format to save to `[json, yaml]`
130
129
 
131
- Returns
132
- -------
133
- No returns
134
-
135
- Exceptions
136
- ----------
130
+ Raises
131
+ ------
137
132
  PluginRegistryError
138
133
  If `error_message` has contents, then raise PluginRegistryError(error_message).
139
134
  The `error_message` string will collect and report on all errors within
@@ -263,7 +258,9 @@ def registry_sanity_check(plugin_packages, save_type, namespace):
263
258
  raise PluginRegistryError(error_message)
264
259
 
265
260
 
266
- def check_plugin_exists(package, plugins, interface_name, plugin_name, relpath):
261
+ def check_plugin_exists(
262
+ package: str, plugins: dict, interface_name: str, plugin_name: str, relpath: str
263
+ ) -> str:
267
264
  """Check if plugin already exists. If it does raise a `PluginRegistryError`.
268
265
 
269
266
  Note this only checks for duplicate plugins within a single plugin package.
@@ -272,14 +269,16 @@ def check_plugin_exists(package, plugins, interface_name, plugin_name, relpath):
272
269
 
273
270
  Parameters
274
271
  ----------
275
- package: str
272
+ package : str
276
273
  The package being tested against
277
- plugins: dict
274
+ plugins : dict
278
275
  A dictionary object of all installed plugins in the current plugin package.
279
- interface_name: str
276
+ interface_name : str
280
277
  A string representing the interface being checked against
281
- plugin_name: str
278
+ plugin_name : str
282
279
  A string representing the name of the plugin within the interface
280
+ relpath : str
281
+ The relative path to the plugin being checked.
283
282
 
284
283
  Returns
285
284
  -------
@@ -300,21 +299,19 @@ def check_plugin_exists(package, plugins, interface_name, plugin_name, relpath):
300
299
  return ""
301
300
 
302
301
 
303
- def write_registered_plugins(pkg_dir, plugins, save_type):
302
+ def write_registered_plugins(
303
+ pkg_dir: str | Path, plugins: dict, save_type: str
304
+ ) -> None:
304
305
  """Write dictionary of all plugins available from installed packages.
305
306
 
306
307
  Parameters
307
308
  ----------
308
- pkg_dir: str
309
+ pkg_dir : str
309
310
  Path in which to write registered_plugins
310
- plugins: dict
311
+ plugins : dict
311
312
  A dictionary object of all installed package plugins
312
- save_type: str
313
+ save_type : str
313
314
  The file format to save to [json, yaml]
314
-
315
- Returns
316
- -------
317
- No returns, file written to `pkg_dir`
318
315
  """
319
316
  if save_type == "yaml":
320
317
  reg_plug_abspath = os_join(pkg_dir, "registered_plugins.yaml")
@@ -347,7 +344,9 @@ def write_registered_plugins(pkg_dir, plugins, save_type):
347
344
  pass
348
345
 
349
346
 
350
- def create_plugin_registries(plugin_packages, save_type, namespace):
347
+ def create_plugin_registries(
348
+ plugin_packages: list, save_type: str, namespace: str
349
+ ) -> None:
351
350
  """Generate all plugin paths associated with every installed packages.
352
351
 
353
352
  These paths include text plugins, class_based plugins
@@ -357,13 +356,13 @@ def create_plugin_registries(plugin_packages, save_type, namespace):
357
356
 
358
357
  Parameters
359
358
  ----------
360
- plugin_packages: list EntryPoints
359
+ plugin_packages : list EntryPoints
361
360
  A list of EntryPoints pointing to each installed package --> ie.
362
361
  [EntryPoint(name='pluginify', value='pluginify',
363
362
  group='pluginify.plugin_packages'), ...]
364
- save_type: str
363
+ save_type : str
365
364
  The file format to save to [json, yaml]
366
- namespace: str
365
+ namespace : str
367
366
  Namespace that your plugin packages fall under. The argument parser defaults
368
367
  this value to 'pluginify.plugin_packages', but a user can create separate
369
368
  namespaces if developing interfaces outside of pluginify.
@@ -371,8 +370,8 @@ def create_plugin_registries(plugin_packages, save_type, namespace):
371
370
  # It appears when there is *.egg-info directory, it picks that package up
372
371
  # twice in the list. If the same package path exists twice, only keep one
373
372
  # of them. This appears to be a bug with Python 3.9 entry points.
374
- pkg_dirs = []
375
- unique_package_entry_points = []
373
+ pkg_dirs: list = []
374
+ unique_package_entry_points: list = []
376
375
  # Note we only use ep.value (resources.files finds the actual plugins),
377
376
  # so we do not need to worry about saving the "wrong" package here.
378
377
  # We are actually looping through all the files in each package, so
@@ -391,7 +390,7 @@ def create_plugin_registries(plugin_packages, save_type, namespace):
391
390
  for pkg in unique_package_entry_points:
392
391
  # This is passed by reference and populated with each call to parse
393
392
  # plugin packages.
394
- plugins = {
393
+ plugins: dict = {
395
394
  "text_based": {},
396
395
  "yaml_based": {},
397
396
  "class_based": {},
@@ -408,7 +407,7 @@ def create_plugin_registries(plugin_packages, save_type, namespace):
408
407
  # that holds all plugins, and the package_schema entry point directly
409
408
  # to the directory that holds all scheam, so the subdirectories are
410
409
  # not hard coded here in the create_plugin_registries code.
411
- pkg_plugin_path = resources.files(package) / "plugins"
410
+ pkg_plugin_path = Path(str(resources.files(package) / "plugins"))
412
411
  pkg_dir = str(resources.files(package))
413
412
  # Generate a path to the parent directory where we should write the plugin
414
413
  # registry file for this package to.
@@ -465,7 +464,13 @@ def create_plugin_registries(plugin_packages, save_type, namespace):
465
464
  registry_sanity_check(unique_package_entry_points, save_type, namespace)
466
465
 
467
466
 
468
- def parse_plugin_paths(plugin_paths, package, package_dir, plugins, namespace):
467
+ def parse_plugin_paths(
468
+ plugin_paths: dict,
469
+ package: str,
470
+ package_dir: str,
471
+ plugins: dict,
472
+ namespace: str,
473
+ ) -> str:
469
474
  """Parse the plugin_paths provided from the current installed package.
470
475
 
471
476
  Then, add them to the plugins dictionary based on the path of the plugin.
@@ -474,22 +479,22 @@ def parse_plugin_paths(plugin_paths, package, package_dir, plugins, namespace):
474
479
 
475
480
  Parameters
476
481
  ----------
477
- plugin_paths: dict
482
+ plugin_paths : dict
478
483
  A dictionary of filepaths, with keys referring to the type of plugin
479
- package: str
484
+ package : str
480
485
  The current package being parsed
481
- package_dir: str
486
+ package_dir : str
482
487
  The path to the current package (for determining relative paths)
483
- plugins: dict
488
+ plugins : dict
484
489
  A dictionary object of all installed package plugins
485
- namespace: str
490
+ namespace : str
486
491
  Namespace that your plugin packages fall under. The argument parser defaults
487
492
  this value to 'pluginify.plugin_packages', but a user can create separate
488
493
  namespaces if developing interfaces outside of pluginify.
489
494
 
490
495
  Returns
491
496
  -------
492
- error_message: str
497
+ error_message : str
493
498
  String containing informative error messages from any plugins that
494
499
  were improperly formatted. An exception will be raised at the
495
500
  very end if error_message is not the empty string - this allows
@@ -541,27 +546,29 @@ def parse_plugin_paths(plugin_paths, package, package_dir, plugins, namespace):
541
546
  return error_message
542
547
 
543
548
 
544
- def add_yaml_plugin(filepath, relpath, package, plugins, namespace):
549
+ def add_yaml_plugin(
550
+ filepath: str, relpath: str, package: str, plugins: dict, namespace: str
551
+ ) -> str:
545
552
  """Add the yaml plugin associated with the filepaths and package to plugins.
546
553
 
547
554
  Parameters
548
555
  ----------
549
- filepath: str
556
+ filepath : str
550
557
  The path of the plugin derived from resouces.files(package) / plugin
551
- relpath: str
558
+ relpath : str
552
559
  The relative path to the filepath provided
553
- package: str
560
+ package : str
554
561
  The current package being parsed
555
- plugins: dict
562
+ plugins : dict
556
563
  A dictionary object of all installed package plugins
557
- namespace: str
564
+ namespace : str
558
565
  Namespace that your plugin packages fall under. The argument parser defaults
559
566
  this value to 'pluginify.plugin_packages', but a user can create separate
560
567
  namespaces if developing interfaces outside of pluginify.
561
568
 
562
569
  Returns
563
570
  -------
564
- error_message: str
571
+ error_message : str
565
572
  String containing informative error messages from any plugins that
566
573
  were improperly formatted. An exception will be raised at the
567
574
  very end if error_message is not the empty string - this allows
@@ -696,7 +703,7 @@ def add_yaml_plugin(filepath, relpath, package, plugins, namespace):
696
703
  # if not family:
697
704
  # family = plugins["product_defaults"][pd]["family"]
698
705
  plugins[interface_name][subplg_source][subplg_product] = {
699
- "docstring": str(format_docstring(docstring)),
706
+ "docstring": str(format_docstring(docstring or "")),
700
707
  "family": family,
701
708
  "interface": interface_module.name,
702
709
  "package": plugin["package"],
@@ -737,21 +744,21 @@ def add_yaml_plugin(filepath, relpath, package, plugins, namespace):
737
744
  return error_message
738
745
 
739
746
 
740
- def add_text_plugin(package, relpath, plugins):
747
+ def add_text_plugin(package: str, relpath: str, plugins: dict) -> str:
741
748
  """Add all text plugins into plugin registries.
742
749
 
743
750
  Parameters
744
751
  ----------
745
- package: str
752
+ package : str
746
753
  The current package being parsed
747
- relpath: str
754
+ relpath : str
748
755
  The relpath path to the text plugin
749
- plugins: dict
756
+ plugins : dict
750
757
  A dictionary object of all installed package plugins
751
758
 
752
759
  Returns
753
760
  -------
754
- error_message: str
761
+ error_message : str
755
762
  String containing informative error messages from any plugins that
756
763
  were improperly formatted. An exception will be raised at the
757
764
  very end if error_message is not the empty string - this allows
@@ -780,8 +787,12 @@ def add_text_plugin(package, relpath, plugins):
780
787
 
781
788
 
782
789
  def collect_module_plugin_metadata(
783
- module, module_name, package, relpath, error_message
784
- ):
790
+ module: Any,
791
+ module_name: str,
792
+ package: str,
793
+ relpath: str,
794
+ error_message: str,
795
+ ) -> dict | str:
785
796
  """Collect metadata for the module-based plugin for the plugin registry.
786
797
 
787
798
  Metadata necessary to collect here includes the interface of the plugin, the
@@ -793,27 +804,27 @@ def collect_module_plugin_metadata(
793
804
 
794
805
  Parameters
795
806
  ----------
796
- module: ModuleType
797
- - The object of the module which has been loaded in realtime. This should have
798
- top level attributes such as 'interface', 'name', 'family' and so on.
799
- module_name: str
800
- - The name of the .py file.
801
- package: str
802
- - The name of the package this plugin comes from
803
- relpath: str
804
- - The relative path to the plugin found in 'package'
805
- error_message: str
806
- - The current state of errors found during building the registry, stored as a
807
- string.
807
+ module : ModuleType
808
+ The object of the module which has been loaded in realtime. This should have
809
+ top level attributes such as 'interface', 'name', 'family' and so on.
810
+ module_name : str
811
+ The name of the .py file.
812
+ package : str
813
+ The name of the package this plugin comes from
814
+ relpath : str
815
+ The relative path to the plugin found in 'package'
816
+ error_message : str
817
+ The current state of errors found during building the registry, stored as a
818
+ string.
808
819
 
809
820
  Returns
810
821
  -------
811
- metadata: dict
812
- - A dictionary of metadata for the provided plugin class to store in the plugin
813
- registry.
814
- error_message: str
815
- - The current state of errors found during building the registry, stored as a
816
- string.
822
+ metadata : dict
823
+ A dictionary of metadata for the provided plugin class to store in the plugin
824
+ registry.
825
+ error_message : str
826
+ The current state of errors found during building the registry, stored as a
827
+ string.
817
828
  """
818
829
  # Try to get "interface" variable from the module. This is required
819
830
  # on ALL files within the python module based plugins directory, to
@@ -884,7 +895,7 @@ def collect_module_plugin_metadata(
884
895
  return metadata
885
896
 
886
897
 
887
- def collect_class_plugin_metadata(plugin_class):
898
+ def collect_class_plugin_metadata(plugin_class, package, relpath, error_message):
888
899
  """Collect metadata linked to the class plugin in 'module' for the plugin registry.
889
900
 
890
901
  Metadata necessary to collect here includes the interface of the plugin, the
@@ -893,24 +904,60 @@ def collect_class_plugin_metadata(plugin_class):
893
904
 
894
905
  Parameters
895
906
  ----------
896
- plugin_class: Object, default=None
897
- - The plugin class from 'module' to collect metadata from. If None, this
898
- function will attempt to locate the correct plugin class from the provided
899
- module.
907
+ plugin_class : Object, default=None
908
+ The plugin class from 'module' to collect metadata from. If None, this
909
+ function will attempt to locate the correct plugin class from the provided
910
+ module.
900
911
 
901
912
  Returns
902
913
  -------
903
- metadata: dict
904
- - A dictionary of metadata for the provided plugin class to store in the plugin
905
- registry.
914
+ metadata : dict
915
+ A dictionary of metadata for the provided plugin class to store in the plugin
916
+ registry.
906
917
  """
907
- # NOTE: Probably should add some attribute checks here as
908
- # 'collect_module_plugin_metadata' does
918
+ error_occurred = False
919
+
920
+ try:
921
+ family = plugin_class.family
922
+ except AttributeError:
923
+ if not getattr(plugin_class, "data_tree"):
924
+ error_occurred = True
925
+ error_message += (
926
+ f"Error: missing 'family' attribute in non-data-tree class based "
927
+ f"plugin found at '{relpath}' in package '{package}'. This attribute "
928
+ "must be specified for non-data-tree class-based plugins."
929
+ )
930
+ else:
931
+ family = "N/A: data tree based plugin"
932
+
933
+ try:
934
+ name = plugin_class.name
935
+ except AttributeError:
936
+ error_occurred = True
937
+ error_message += (
938
+ f"Error: missing 'name' attribute in class based "
939
+ f"plugin found at '{relpath}' in package '{package}'. This attribute must "
940
+ "be specified in class-based plugins."
941
+ )
942
+
943
+ try:
944
+ interface = plugin_class.interface
945
+ except AttributeError:
946
+ error_occurred = True
947
+ error_message += (
948
+ f"Error: missing 'interface' attribute in class based "
949
+ f"plugin found at '{relpath}' in package '{package}'. This attribute must "
950
+ "be specified in class-based plugins."
951
+ )
952
+
953
+ if error_occurred:
954
+ return error_message
955
+
909
956
  metadata = {
910
957
  "docstring": format_docstring(plugin_class.__doc__),
911
- "name": plugin_class.name,
912
- "family": plugin_class.family,
913
- "interface": plugin_class.interface,
958
+ "name": name,
959
+ "family": family,
960
+ "interface": interface,
914
961
  "plugin_type": "class_based",
915
962
  "signature": str(signature(plugin_class.call)),
916
963
  "is_derived_from_module": False,
@@ -919,7 +966,7 @@ def collect_class_plugin_metadata(plugin_class):
919
966
  return metadata
920
967
 
921
968
 
922
- def add_class_plugin(package, relpath, plugins):
969
+ def add_class_plugin(package: str, relpath: str, plugins: dict) -> str:
923
970
  """Add the class-based plugin associated with the filepaths and package to plugins.
924
971
 
925
972
  NOTE: This function will work for 'legacy' module-based plugins that are dynamically
@@ -930,16 +977,16 @@ def add_class_plugin(package, relpath, plugins):
930
977
 
931
978
  Parameters
932
979
  ----------
933
- package: str
980
+ package : str
934
981
  The current package being parsed
935
- relpath: str
982
+ relpath : str
936
983
  The relpath path to the class-based plugin
937
- plugins: dict
984
+ plugins : dict
938
985
  A dictionary object of all installed package plugins
939
986
 
940
987
  Returns
941
988
  -------
942
- error_message: str
989
+ error_message : str
943
990
  String containing informative error messages from any plugins that
944
991
  were improperly formatted. An exception will be raised at the
945
992
  very end if error_message is not the empty string - this allows
@@ -970,7 +1017,11 @@ def add_class_plugin(package, relpath, plugins):
970
1017
  module_path = f"{package}.{module_path}"
971
1018
  abspath = resources.files(package) / relpath
972
1019
 
973
- spec = util.spec_from_file_location(module_path, abspath)
1020
+ spec = util.spec_from_file_location(module_path, str(abspath))
1021
+ if spec is None or spec.loader is None:
1022
+ raise PluginRegistryError(
1023
+ f"Could not create a module spec for '{module_path}' at '{abspath}'."
1024
+ )
974
1025
  module = util.module_from_spec(spec)
975
1026
  # Attempting importing module, catch ImportError
976
1027
  # We have to fix these to be able to import the module to
@@ -1022,7 +1073,14 @@ def add_class_plugin(package, relpath, plugins):
1022
1073
  error_message += str(plugin_class_error_message)
1023
1074
  return error_message
1024
1075
 
1025
- plugin_metadata = collect_class_plugin_metadata(PLUGIN_CLASS)
1076
+ plugin_metadata = collect_class_plugin_metadata(
1077
+ PLUGIN_CLASS, package, relpath, error_message
1078
+ )
1079
+ if isinstance(plugin_metadata, str):
1080
+ # Error message was returned instead of a metadata dictionary. Just return
1081
+ # that message now.
1082
+ return plugin_metadata
1083
+
1026
1084
  interface_name = plugin_metadata["interface"]
1027
1085
  name = plugin_metadata.pop("name")
1028
1086
 
pluginify/errors.py CHANGED
@@ -1,16 +1,24 @@
1
1
  # # # This source code is subject to the license referenced at
2
2
  # # # https://github.com/NRLMMD-GEOIPS.
3
3
 
4
- """Pluginify error module."""
4
+ """Exceptions raised by the pluginify package.
5
+
6
+ Provides two error classes:
7
+
8
+ - ``PluginError`` — raised when an individual plugin is malformed, missing
9
+ required attributes, or otherwise invalid.
10
+ - ``PluginRegistryError`` — raised when a plugin registry itself is missing,
11
+ corrupt, or contains duplicate/conflicting entries across packages.
12
+ """
5
13
 
6
14
 
7
15
  class PluginError(Exception):
8
- """Exception to be raised when there is an error in a plugin."""
16
+ """Exception raised for errors related to an individual plugin."""
9
17
 
10
18
  pass
11
19
 
12
20
 
13
21
  class PluginRegistryError(Exception):
14
- """Exception to be raised when there is an error in a plugin registry."""
22
+ """Exception raised for errors related to a plugin registry."""
15
23
 
16
24
  pass
@@ -1,7 +1,16 @@
1
1
  # # # This source code is subject to the license referenced at
2
2
  # # # https://github.com/NRLMMD-GEOIPS.
3
3
 
4
- """Initialization module for pluginify interfaces."""
4
+ """Interface discovery and initialization module for pluginify.
5
+
6
+ Imports all available class-based and YAML-based interface instances so that
7
+ other parts of the pluginify ecosystem can discover them via
8
+ ``from pluginify.interfaces import <interface_name>``.
9
+
10
+ The ``class_based_interfaces`` and ``yaml_based_interfaces`` lists are used by
11
+ ``PluginRegistryValidator.validate_registry_interfaces`` to validate that
12
+ registry entries reference known interface names.
13
+ """
5
14
 
6
15
  from pluginify.interfaces.class_based.data_modifiers import data_modifiers # noqa: F401
7
16
  from pluginify.interfaces.yaml_based.configs import configs # noqa: F401