Overhauled v2 plugin attaching

This commit is contained in:
jtc42 2019-11-19 19:19:54 +00:00
parent b8f6193f7b
commit 3c096e9b53
6 changed files with 167 additions and 135 deletions

View file

@ -39,10 +39,7 @@ def construct_blueprint(microscope_obj):
all_routes = [] all_routes = []
# For each plugin attached to the microscope object # For each plugin attached to the microscope object
for plugin_representation in microscope_obj.plugins.active: for plugin_name, plugin_obj in microscope_obj.plugins._legacy_plugins.items():
plugin_obj = plugin_representation["plugin"]
plugin_name = plugin_representation["name"]
# If plugin contains valid endpoints # If plugin contains valid endpoints
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):

View file

@ -20,20 +20,34 @@ def plugins_representation(plugin_loader_object: PluginLoader):
dict: Dictionary representation of all plugins dict: Dictionary representation of all plugins
""" """
plugins = [] plugins = []
print(plugin_loader_object.active)
for plugin in plugin_loader_object.active: for plugin in plugin_loader_object.active:
logging.info(f"Representing plugin {plugin._name}")
d = { d = {
"name": plugin["name"], "name": plugin._name,
"plugin": str(plugin["plugin"]), "plugin": str(plugin),
"routes": plugin["routes"], "views": {},
"form": plugin["form"], "form": plugin.form,
} }
for route in d["routes"]: for view_id, view in plugin.views.items():
route_id = route["id"] logging.info(f"Representing view {view_id}")
uri = url_for(f"v2_plugins_blueprint.{route_id}") uri = url_for(f"v2_plugins_blueprint.{view_id}")
route["links"]["self"] = uri # Make links dictionary if it doesn't yet exist
view_d = {
"links": {"self": uri}
}
d["views"][view_id] = view_d
print("\n")
print(d)
print("\n")
plugins.append(d) plugins.append(d)
print(plugins)
return plugins return plugins
@ -66,69 +80,20 @@ def construct_blueprint(microscope_obj):
all_routes = [] all_routes = []
# For each plugin attached to the microscope object # For each plugin attached to the microscope object
for plugin_representation in microscope_obj.plugins.active: for plugin in microscope_obj.plugins.active:
plugin_obj = plugin_representation["plugin"] for plugin_view_id, plugin_view in plugin.views.items():
plugin_name = plugin_representation["name"] # Add route to the plugins blueprint
blueprint.add_url_rule(
# If plugin contains valid endpoints plugin_view["rule"],
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): view_func=plugin_view["view"].as_view(
plugin_view_id,
# We'll keep a record of how each route was expanded microscope=microscope_obj,
expanded_routes = {} plugin=plugin,
),
# For each defined endpoint )
for view_route, view_class in plugin_obj.api_views.items():
# Remove all leading slashes from view route
cleaned_route = view_route
while cleaned_route[0] == "/":
cleaned_route = cleaned_route[1:]
# Construct a full view route from the plugin name
full_view_route = "/{}/{}".format(plugin_name, cleaned_route)
logging.debug(full_view_route)
# Record how the view_route got expanded
expanded_routes[view_route] = full_view_route
# Check if endpoint name clashes
if full_view_route not in all_routes and issubclass(
view_class, MicroscopeViewPlugin
):
# Add route to main route dictionary
all_routes.append(full_view_route)
# Create a Python-safe name for the route
plugin_route_id = "plugin{}".format(full_view_route).replace(
"/", "_"
)
# Add route to the plugins blueprint
blueprint.add_url_rule(
full_view_route,
view_func=view_class.as_view(
plugin_route_id,
microscope=microscope_obj,
plugin=plugin_obj,
),
)
# Add route to the plugin representation dictionary
route_representation = {
"id": plugin_route_id,
"route": full_view_route,
"links": {},
}
plugin_representation["routes"].append(route_representation)
else:
warnings.warn(
"An endpoint /{} has already been loaded. Skipping {}.".format(
full_view_route, view_class
)
)
"""
# If plugin includes an API form # If plugin includes an API form
if hasattr(plugin_obj, "api_form") and isinstance( if hasattr(plugin_obj, "api_form") and isinstance(
plugin_obj.api_form, dict plugin_obj.api_form, dict
@ -152,9 +117,6 @@ def construct_blueprint(microscope_obj):
# Store the complete form in Microscope().plugin.form # Store the complete form in Microscope().plugin.form
plugin_representation["form"] = api_form_info plugin_representation["form"] = api_form_info
print(microscope_obj.plugins.forms) print(microscope_obj.plugins.forms)
"""
else:
warnings.warn(
"No valid 'api_views' dictionary found in {}".format(plugin_obj)
)
return blueprint return blueprint

View file

@ -58,6 +58,8 @@ class ScanPlugin(MicroscopePlugin):
api_views = {"/tile": TileScanAPI} api_views = {"/tile": TileScanAPI}
def __init__(self): def __init__(self):
MicroscopePlugin.__init__(self)
self.images_to_be_captured: int = 1 self.images_to_be_captured: int = 1
update_task_data({"images_to_be_captured": self.images_to_be_captured}) update_task_data({"images_to_be_captured": self.images_to_be_captured})

View file

@ -4,6 +4,7 @@ import os
import inspect import inspect
import logging import logging
from openflexure_microscope.utilities import camel_to_snake, camel_to_spine
from openflexure_microscope.api.views import MicroscopeViewPlugin from openflexure_microscope.api.views import MicroscopeViewPlugin
@ -83,7 +84,6 @@ def name_from_module(plugin_path):
def load_plugin_module(plugin_path): def load_plugin_module(plugin_path):
# If the loader was found (i.e. plugin probably exists) # If the loader was found (i.e. plugin probably exists)
if check_module(plugin_path): if check_module(plugin_path):
try: try:
@ -140,50 +140,6 @@ def class_from_map(plugin_map):
return load_plugin_class(*plugin_arr) return load_plugin_class(*plugin_arr)
def expand_views(plugin_object, plugin_name):
routes = []
# If plugin contains valid endpoints
if hasattr(plugin_object, "api_views") and isinstance(plugin_object.api_views, dict):
# We'll keep a record of how each route was expanded
expanded_routes = {}
# For each defined endpoint
for view_route, view_class in plugin_object.api_views.items():
# Remove all leading slashes from view route
cleaned_route = view_route
while cleaned_route[0] == "/":
cleaned_route = cleaned_route[1:]
# Construct a full view route from the plugin name
full_view_route = "/{}/{}".format(plugin_name, cleaned_route)
logging.debug(full_view_route)
# Record how the view_route got expanded
expanded_routes[view_route] = full_view_route
# Check if endpoint name clashes
if issubclass(view_class, MicroscopeViewPlugin):
# Create a Python-safe name for the route
plugin_route_id = "plugin{}".format(full_view_route).replace(
"/", "_"
)
# Add route to the plugin representation dictionary
route_representation = {
"id": plugin_route_id,
"route": full_view_route,
"links": {},
}
routes.append(route_representation)
return routes
class PluginLoader(object): class PluginLoader(object):
""" """
A mount-point for all loaded plugins. Attaches to a Microscope object. A mount-point for all loaded plugins. Attaches to a Microscope object.
@ -195,6 +151,7 @@ class PluginLoader(object):
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self._plugins = [] # List of plugin objects self._plugins = [] # List of plugin objects
self._legacy_plugins = {} # DEPRECATED: Dictionary of plugins with old names
self.forms = [] # List of plugin forms self.forms = [] # List of plugin forms
logging.info("Creating plugin mount") logging.info("Creating plugin mount")
@ -221,13 +178,15 @@ class PluginLoader(object):
if plugin_class is not None: if plugin_class is not None:
logging.debug(f"Loading plugin class {plugin_class}, {plugin_name}")
plugin_name_python_safe = plugin_name.replace("/", "_") plugin_name_python_safe = plugin_name.replace("/", "_")
if plugin_class and plugin_name: if plugin_class and plugin_name:
plugin_object = plugin_class() plugin_object = plugin_class()
if hasattr( if hasattr(
self, plugin_name self, plugin_name
): # If a plugin with the same name is already attached. ): # If a plugin with the same name is already attached.
logging.warning( logging.warning(
ConColors.WARNING ConColors.WARNING
@ -238,24 +197,19 @@ class PluginLoader(object):
) )
elif isinstance( elif isinstance(
plugin_object, MicroscopePlugin plugin_object, BasePlugin
): # If plugin_object is an instance of MicroscopePlugin ): # If plugin_object is an instance of MicroscopePlugin
# Attach plugin_object to the plugin mount # Attach plugin_object to the plugin mount
setattr(self, plugin_name_python_safe, plugin_object) setattr(self, plugin_name_python_safe, plugin_object)
# Store the plugin object, and it's properties # Store the plugin object, and it's properties
self._plugins.append( self._plugins.append(plugin_object)
{ # DEPRECATED: Store the plugin with it's old name
"name": plugin_name, self._legacy_plugins[plugin_name_python_safe] = plugin_object
"python_name": plugin_name_python_safe,
"plugin": plugin_object,
"routes": [],
"form": None,
}
)
# Grant plugin access to the hardware # Grant plugin access to the hardware
plugin_object.microscope = self.parent if isinstance(plugin_object, MicroscopePlugin):
plugin_object.microscope = self.parent
logging.info( logging.info(
ConColors.OKGREEN ConColors.OKGREEN
@ -267,7 +221,112 @@ class PluginLoader(object):
logging.warning(f"Error loading plugin {plugin_map}. Moving on.") logging.warning(f"Error loading plugin {plugin_map}. Moving on.")
class MicroscopePlugin: class BasePlugin:
"""
Parent class for all plugins.
Handles binding route views and forms.
"""
def __init__(self):
self._views = {}
self._rules = {}
self._form = None
# If old api_views dictionary is found
if hasattr(self, "api_views"):
# Convert to new format
self._convert_old_api_views()
# If old api_form dictionary is found
if hasattr(self, "api_form"):
# Convert to new format
self._convert_old_api_form()
print("\n\n NAME: " + self._name + "\n\n")
print(self.views)
print(self.form)
@property
def views(self):
return self._views
def add_view(self, rule, view_class):
# Remove all leading slashes from view route
cleaned_rule = rule
while cleaned_rule[0] == "/":
cleaned_rule = cleaned_rule[1:]
# Expand the rule to include plugin name
full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule)
# Create a Python-safe route ID
view_id = cleaned_rule.replace("/", "_")
full_view_id = f"{self._name_python_safe}_{view_id}"
# Store route information in a dictionary
d = {
"rule": full_rule,
"view": view_class
}
# Add view to private views dictionary
self._views[full_view_id] = d
# Store the rule expansion information
self._rules[rule] = self._views[full_view_id]
@property
def form(self):
if not self._form:
return None
api_form_info = copy.deepcopy(self._form)
api_form_info["id"] = self._name
if "forms" in api_form_info and isinstance(
api_form_info["forms"], list
):
for form in api_form_info["forms"]:
if "route" in form and form["route"] in self._rules.keys():
form["route"] = self._rules[form["route"]]["rule"]
else:
logging.warn(
"No valid expandable route found for {}".format(
form["route"]
)
)
return api_form_info
def set_form(self, form_dictionary: dict):
self._form = form_dictionary
def _convert_old_api_views(self):
for view_route, view_class in self.api_views.items():
self.add_view(view_route, view_class)
def _convert_old_api_form(self):
self.set_form(self.api_form)
@property
def _name(self):
return self.__class__.__name__
@property
def _name_python_safe(self):
return camel_to_snake(self._name)
@property
def _name_uri_safe(self):
return camel_to_spine(self._name)
def _full_name(self):
module = self.__class__.__module__
if module is None or module == str.__class__.__module__:
return self.__class__.__name__ # Avoid reporting __builtin__
else:
return module + '.' + self.__class__.__name__
class MicroscopePlugin(BasePlugin):
""" """
Parent class for all microscope plugins. Parent class for all microscope plugins.
@ -278,6 +337,7 @@ class MicroscopePlugin:
api_views = {} # Initially empty dictionary of API views associated with the plugin api_views = {} # Initially empty dictionary of API views associated with the plugin
def __init__(self): def __init__(self):
BasePlugin.__init__(self)
self.microscope = ( self.microscope = (
None None
) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object ) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object

View file

@ -23,6 +23,8 @@ class ExamplePlugin(MicroscopePlugin):
api_views = {"/do": DoAPI, "/task": TaskAPI} api_views = {"/do": DoAPI, "/task": TaskAPI}
def __init__(self): def __init__(self):
MicroscopePlugin.__init__(self)
self.val_int = 10 self.val_int = 10
self.val_str = "Hello" self.val_str = "Hello"
self.val_radio = "First" self.val_radio = "First"

View file

@ -1,3 +1,4 @@
import re
import copy import copy
import operator import operator
from collections import abc from collections import abc
@ -5,6 +6,14 @@ from functools import reduce
from contextlib import contextmanager from contextlib import contextmanager
def camel_to_snake(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def camel_to_spine(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1-\2', s1).lower()
@contextmanager @contextmanager
def set_properties(obj, **kwargs): def set_properties(obj, **kwargs):
"""A context manager to set, then reset, certain properties of an object. """A context manager to set, then reset, certain properties of an object.