mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-30 06:48:47 +02:00
[core] Plugins: Introduction of NodePluginManager
The NodePluginManager provides a way for registering and managing node plugins within Meshroom. This also provides a way for other components to interact with the plugins to understand whether a plugin is available or not.
This commit is contained in:
parent
648b0950b8
commit
02b85cb273
11 changed files with 600 additions and 149 deletions
8
meshroom/_plugins/__init__.py
Normal file
8
meshroom/_plugins/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
""" Plugins.
|
||||
"""
|
||||
# Plugins
|
||||
from .node import NodePluginManager
|
||||
from .base import Status, Pluginator
|
||||
|
||||
|
||||
__all__ = ["NodePluginManager", "Status", "Pluginator"]
|
92
meshroom/_plugins/base.py
Normal file
92
meshroom/_plugins/base.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
""" Base functionality for Plugins.
|
||||
"""
|
||||
# Types
|
||||
from typing import List
|
||||
|
||||
# STD
|
||||
from contextlib import contextmanager
|
||||
import enum
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import pkgutil
|
||||
|
||||
|
||||
class Status(enum.IntEnum):
|
||||
""" Enum describing the state of the plugin.
|
||||
"""
|
||||
|
||||
# UNLOADED or NOT Available - Describing that the plugin is not available in the current set of plugins
|
||||
UNLOADED = -1
|
||||
|
||||
# ERRORED describes that the plugin exists but could not be loaded due to errors with the structure
|
||||
ERRORED = 0
|
||||
|
||||
# LOADED describes that the plugin is currently loaded and is fully functional
|
||||
LOADED = 1
|
||||
|
||||
|
||||
class Pluginator:
|
||||
""" The common plugin utilities.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def add_to_path(_p):
|
||||
""" A Context Manager to add the provided path to Python's sys.path temporarily.
|
||||
"""
|
||||
import sys # pylint: disable=import-outside-toplevel
|
||||
old_path = sys.path
|
||||
sys.path = sys.path[:]
|
||||
sys.path.insert(0, _p)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.path = old_path
|
||||
|
||||
@staticmethod
|
||||
def get(folder, packageName, classType) -> List:
|
||||
""" Returns Array of Plugin, each holding the plugin and the module it belongs to.
|
||||
|
||||
Args:
|
||||
folder (str): Path to the Directory.
|
||||
packageName (str): Name of the package to import.
|
||||
classType (desc.Node | BaseSubmitter): The base type of plugin which is being imported.
|
||||
"""
|
||||
pluginTypes = []
|
||||
errors = []
|
||||
|
||||
# temporarily add folder to python path
|
||||
with Pluginator.add_to_path(folder):
|
||||
# import node package
|
||||
package = importlib.import_module(packageName)
|
||||
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
|
||||
packageVersion = getattr(package, "__version__", None)
|
||||
|
||||
for importer, pluginName, ispkg in pkgutil.iter_modules(package.__path__):
|
||||
pluginModuleName = '.' + pluginName
|
||||
|
||||
try:
|
||||
pluginMod = importlib.import_module(pluginModuleName, package=package.__name__)
|
||||
plugins = [plugin for name, plugin in inspect.getmembers(pluginMod, inspect.isclass)
|
||||
if plugin.__module__ == '{}.{}'.format(package.__name__, pluginName)
|
||||
and issubclass(plugin, classType)]
|
||||
if not plugins:
|
||||
logging.warning("No class defined in plugin: {}".format(pluginModuleName))
|
||||
|
||||
# Update the package name and version on the plugin
|
||||
for p in plugins:
|
||||
p.packageName = packageName
|
||||
p.packageVersion = packageVersion
|
||||
|
||||
# Extend all of the plugins
|
||||
pluginTypes.extend(plugins)
|
||||
except Exception as exc:
|
||||
errors.append(' * {}: {}'.format(pluginName, str(exc)))
|
||||
|
||||
if errors:
|
||||
logging.warning('== The following "{package}" plugins could not be loaded ==\n'
|
||||
'{errorMsg}\n'
|
||||
.format(package=packageName, errorMsg='\n'.join(errors)))
|
||||
|
||||
return pluginTypes
|
283
meshroom/_plugins/node.py
Normal file
283
meshroom/_plugins/node.py
Normal file
|
@ -0,0 +1,283 @@
|
|||
""" Node plugins.
|
||||
"""
|
||||
# Types
|
||||
from typing import Dict, List
|
||||
from types import ModuleType
|
||||
|
||||
# STD
|
||||
import importlib
|
||||
import logging
|
||||
import pkgutil
|
||||
import sys
|
||||
|
||||
# Internal
|
||||
from meshroom.core import desc
|
||||
|
||||
# Plugins
|
||||
from .base import Status, Pluginator
|
||||
|
||||
|
||||
class NodeDescriptor(object):
|
||||
""" Class to describe a Node Plugin.
|
||||
"""
|
||||
|
||||
_DEFAULT_VERSION = "0"
|
||||
|
||||
def __init__(self, name: str, descriptor: desc.Node) -> None:
|
||||
""" Constructor.
|
||||
|
||||
Args:
|
||||
name (str): Name of the Node.
|
||||
descriptor (desc.Node): The Node descriptor.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# Node descriptions
|
||||
self._name: str = name
|
||||
self._descriptor: desc.Node = descriptor
|
||||
|
||||
# Update the Node Descriptor's plugin
|
||||
self._descriptor.plugin = self
|
||||
|
||||
# Module descriptions
|
||||
self._module: ModuleType = sys.modules.get(self._descriptor.__module__)
|
||||
self._version = getattr(self._module, "__version__", self._DEFAULT_VERSION)
|
||||
self._path = self._module.__file__
|
||||
|
||||
self._errors: List[str] = self._validate()
|
||||
|
||||
# Properties
|
||||
name = property(lambda self: self._name)
|
||||
descriptor = property(lambda self: self._descriptor)
|
||||
category = property(lambda self: self._descriptor.category)
|
||||
errors = property(lambda self: self._errors)
|
||||
documentation = property(lambda self: self._descriptor.documentation)
|
||||
version = property(lambda self: self._version)
|
||||
path = property(lambda self: self._path)
|
||||
|
||||
@property
|
||||
def status(self) -> Status:
|
||||
""" Returns the status of the plugin.
|
||||
"""
|
||||
# If no errors -> then it is loaded and available
|
||||
return Status(not self._errors)
|
||||
|
||||
def __repr__(self):
|
||||
""" Represents the Instance.
|
||||
"""
|
||||
return f"NodeDescriptor::{self._name} at {hex(id(self))}"
|
||||
|
||||
def _validate(self) -> List[str]:
|
||||
""" Check that the node has a valid description before being loaded. For the description to be valid, the
|
||||
default value of every parameter needs to correspond to the type of the parameter.
|
||||
|
||||
An empty returned list means that every parameter is valid, and so is the node's description. If it is not
|
||||
valid, the returned list contains the names of the invalid parameters. In case of nested parameters (parameters
|
||||
in groups or lists, for example), the name of the parameter follows the name of the parent attributes.
|
||||
|
||||
For example,
|
||||
If the attribute "x", contained in group "group", is invalid, then it will be added to the list as "group:x".
|
||||
|
||||
Returns:
|
||||
errors (list<str>): the list of invalid parameters if there are any, empty list otherwise.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for param in self._descriptor.inputs:
|
||||
err = param.checkValueTypes()
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
for param in self._descriptor.outputs:
|
||||
# Ignore the output attributes with None as the value
|
||||
if param.value is None:
|
||||
continue
|
||||
|
||||
err = param.checkValueTypes()
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
# Return any errors while validating the input and output attributes
|
||||
return errors
|
||||
|
||||
# Public
|
||||
def reload(self) -> None:
|
||||
""" Reloads the Node.
|
||||
"""
|
||||
# Reload the Module
|
||||
updated = importlib.reload(self._module)
|
||||
|
||||
# Get the Descriptor
|
||||
descriptor = getattr(updated, self._name)
|
||||
|
||||
# Cannot find the current class on the updated module ?
|
||||
if not descriptor:
|
||||
return
|
||||
|
||||
# Update the descriptor and call for validation
|
||||
self._module = updated
|
||||
|
||||
self._descriptor = descriptor
|
||||
self._descriptor.plugin = self
|
||||
|
||||
# Update the errors if any that may have been introduced
|
||||
self._errors = self._validate()
|
||||
|
||||
|
||||
class NodePluginManager(object):
|
||||
""" A Singleton class Managing the Node plugins for Meshroom.
|
||||
"""
|
||||
# Static class instance to ensure we have only one created at all times
|
||||
_instance = None
|
||||
|
||||
# The core class to which the Node plugins belong
|
||||
_CLASS_TYPE = desc.Node
|
||||
|
||||
def __new__(cls):
|
||||
# Verify that the instance we have is of the current type
|
||||
if not isinstance(cls._instance, cls):
|
||||
# Create an instance to work with
|
||||
cls._instance = object.__new__(cls)
|
||||
|
||||
# Init the class parameters
|
||||
# The class parameters need to be initialised outside __init__ as when Cls() gets invoked __init__ gets
|
||||
# called as well, so even when we get the same instance back, the params are updated for this and every
|
||||
# other instance and that what will affect attrs in all places where the current instance is being used
|
||||
cls._instance.init()
|
||||
|
||||
# Return the instantiated instance
|
||||
return cls._instance
|
||||
|
||||
def init(self) -> None:
|
||||
""" Constructor for members.
|
||||
"""
|
||||
self._descriptors: Dict[str: NodeDescriptor] = {} # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
# Properties
|
||||
descriptors = property(lambda self: self._descriptors)
|
||||
|
||||
# Public
|
||||
def registered(self, name: str) -> bool:
|
||||
""" Returns whether the plugin has been registered already or not.
|
||||
|
||||
Args:
|
||||
name (str): Name of the plugin.
|
||||
|
||||
Returns:
|
||||
bool. True if the plugin was registered, else False.
|
||||
"""
|
||||
return name in self._descriptors
|
||||
|
||||
def status(self, name: str) -> Status:
|
||||
""" Returns the current status of the plugin.
|
||||
|
||||
Args:
|
||||
name (str): Name of the plugin.
|
||||
|
||||
Returns:
|
||||
Status. The current status of the plugin.
|
||||
"""
|
||||
# Fetch the plugin Descriptor
|
||||
plugin = self._descriptors.get(name)
|
||||
|
||||
if not plugin:
|
||||
return Status.UNLOADED
|
||||
|
||||
# Return the status from the plugin itself
|
||||
return plugin.status
|
||||
|
||||
def errors(self, name) -> List[str]:
|
||||
""" Returns the Errors on the plugins if there are any.
|
||||
|
||||
Args:
|
||||
name (str): Name of the plugin.
|
||||
|
||||
Returns:
|
||||
list<str>. the list of invalid parameters if there are any, empty list otherwise.
|
||||
"""
|
||||
# Fetch the plugin Descriptor
|
||||
plugin = self._descriptors.get(name)
|
||||
|
||||
if not plugin:
|
||||
return []
|
||||
|
||||
# Return any errors from the plugin side
|
||||
return plugin.errors
|
||||
|
||||
def registerNode(self, descriptor: desc.Node) -> bool:
|
||||
""" Registers a Node into Meshroom.
|
||||
|
||||
Args:
|
||||
descriptor (desc.Node): The Node descriptor.
|
||||
|
||||
Returns:
|
||||
bool. Returns True if the node is registered. False if it is already registered.
|
||||
"""
|
||||
# Plugin name
|
||||
name = descriptor.__name__
|
||||
|
||||
# Already registered ?
|
||||
if self.registered(name):
|
||||
return False
|
||||
|
||||
# Register it
|
||||
self.register(name, descriptor)
|
||||
|
||||
return True
|
||||
|
||||
def unregisterNode(self, descriptor: desc.Node) -> None:
|
||||
""" Unregisters the Node from the Registered Set of Nodes.
|
||||
|
||||
Args:
|
||||
descriptor (desc.Node): The Node descriptor.
|
||||
"""
|
||||
# Plugin name
|
||||
name = descriptor.__name__
|
||||
|
||||
# Ensure that we have this node already present
|
||||
assert name in self._descriptors
|
||||
# Delete the instance
|
||||
del self._descriptors[name]
|
||||
|
||||
def register(self, name: str, descriptor: desc.Node) -> None:
|
||||
""" Registers a Node within meshroom.
|
||||
|
||||
Args:
|
||||
name (str): Name of the Node Plugin.
|
||||
descriptor (desc.Node): The Node descriptor.
|
||||
"""
|
||||
self._descriptors[name] = NodeDescriptor(name, descriptor)
|
||||
|
||||
def descriptor(self, name: str) -> desc.Node:
|
||||
""" Returns the Node Desc for the provided name.
|
||||
|
||||
Args:
|
||||
name (str): Name of the plugin.
|
||||
|
||||
Returns:
|
||||
desc.Node. The Node Desc instance.
|
||||
"""
|
||||
# Returns the plugin for the provided name
|
||||
plugin = self._descriptors.get(name)
|
||||
|
||||
# Plugin not found with the name
|
||||
if not plugin:
|
||||
return None
|
||||
|
||||
# Return the Node Descriptor for the plugin
|
||||
return plugin.descriptor
|
||||
|
||||
def load(self, directory) -> None:
|
||||
""" Loads Node from the provided directory.
|
||||
"""
|
||||
for _, package, ispkg in pkgutil.walk_packages([directory]):
|
||||
if not ispkg:
|
||||
continue
|
||||
|
||||
# Get the plugins from the provided directory and the python package
|
||||
descriptors = Pluginator.get(directory, package, self._CLASS_TYPE)
|
||||
|
||||
for descriptor in descriptors:
|
||||
self.registerNode(descriptor)
|
||||
|
||||
logging.debug('Nodes loaded [{}]: {}'.format(package, ', '.join([d.__name__ for d in descriptors])))
|
|
@ -1,12 +1,8 @@
|
|||
import hashlib
|
||||
from contextlib import contextmanager
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
import logging
|
||||
import pkgutil
|
||||
|
||||
import sys
|
||||
|
||||
|
@ -19,7 +15,7 @@ except Exception:
|
|||
pass
|
||||
|
||||
from meshroom.core.submitter import BaseSubmitter
|
||||
from . import desc
|
||||
from meshroom import _plugins
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO)
|
||||
|
@ -29,10 +25,15 @@ sessionUid = str(uuid.uuid1())
|
|||
|
||||
cacheFolderName = 'MeshroomCache'
|
||||
defaultCacheFolder = os.environ.get('MESHROOM_CACHE', os.path.join(tempfile.gettempdir(), cacheFolderName))
|
||||
nodesDesc = {}
|
||||
submitters = {}
|
||||
pipelineTemplates = {}
|
||||
|
||||
# Manages plugins for Meshroom Nodes
|
||||
pluginManager = _plugins.NodePluginManager()
|
||||
|
||||
# Plugin States
|
||||
PluginStatus = _plugins.Status
|
||||
|
||||
|
||||
def hashValue(value):
|
||||
""" Hash 'value' using sha1. """
|
||||
|
@ -40,100 +41,6 @@ def hashValue(value):
|
|||
return hashObject.hexdigest()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def add_to_path(p):
|
||||
import sys
|
||||
old_path = sys.path
|
||||
sys.path = sys.path[:]
|
||||
sys.path.insert(0, p)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.path = old_path
|
||||
|
||||
|
||||
def loadPlugins(folder, packageName, classType):
|
||||
"""
|
||||
"""
|
||||
|
||||
pluginTypes = []
|
||||
errors = []
|
||||
|
||||
# temporarily add folder to python path
|
||||
with add_to_path(folder):
|
||||
# import node package
|
||||
package = importlib.import_module(packageName)
|
||||
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
|
||||
packageVersion = getattr(package, "__version__", None)
|
||||
|
||||
for importer, pluginName, ispkg in pkgutil.iter_modules(package.__path__):
|
||||
pluginModuleName = '.' + pluginName
|
||||
|
||||
try:
|
||||
pluginMod = importlib.import_module(pluginModuleName, package=package.__name__)
|
||||
plugins = [plugin for name, plugin in inspect.getmembers(pluginMod, inspect.isclass)
|
||||
if plugin.__module__ == '{}.{}'.format(package.__name__, pluginName)
|
||||
and issubclass(plugin, classType)]
|
||||
if not plugins:
|
||||
logging.warning("No class defined in plugin: {}".format(pluginModuleName))
|
||||
|
||||
importPlugin = True
|
||||
for p in plugins:
|
||||
if classType == desc.Node:
|
||||
nodeErrors = validateNodeDesc(p)
|
||||
if nodeErrors:
|
||||
errors.append(" * {}: The following parameters do not have valid default values/ranges: {}"
|
||||
.format(pluginName, ", ".join(nodeErrors)))
|
||||
importPlugin = False
|
||||
break
|
||||
p.packageName = packageName
|
||||
p.packageVersion = packageVersion
|
||||
if importPlugin:
|
||||
pluginTypes.extend(plugins)
|
||||
except Exception as e:
|
||||
errors.append(' * {}: {}'.format(pluginName, str(e)))
|
||||
|
||||
if errors:
|
||||
logging.warning('== The following "{package}" plugins could not be loaded ==\n'
|
||||
'{errorMsg}\n'
|
||||
.format(package=packageName, errorMsg='\n'.join(errors)))
|
||||
return pluginTypes
|
||||
|
||||
|
||||
def validateNodeDesc(nodeDesc):
|
||||
"""
|
||||
Check that the node has a valid description before being loaded. For the description
|
||||
to be valid, the default value of every parameter needs to correspond to the type
|
||||
of the parameter.
|
||||
An empty returned list means that every parameter is valid, and so is the node's description.
|
||||
If it is not valid, the returned list contains the names of the invalid parameters. In case
|
||||
of nested parameters (parameters in groups or lists, for example), the name of the parameter
|
||||
follows the name of the parent attributes. For example, if the attribute "x", contained in group
|
||||
"group", is invalid, then it will be added to the list as "group:x".
|
||||
|
||||
Args:
|
||||
nodeDesc (desc.Node): description of the node
|
||||
|
||||
Returns:
|
||||
errors (list): the list of invalid parameters if there are any, empty list otherwise
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for param in nodeDesc.inputs:
|
||||
err = param.checkValueTypes()
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
for param in nodeDesc.outputs:
|
||||
if param.value is None:
|
||||
continue
|
||||
err = param.checkValueTypes()
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
class Version(object):
|
||||
"""
|
||||
Version provides convenient properties and methods to manipulate and compare versions.
|
||||
|
@ -149,7 +56,10 @@ class Version(object):
|
|||
self.status = str()
|
||||
elif len(args) == 1:
|
||||
versionName = args[0]
|
||||
if isinstance(versionName, str):
|
||||
if not versionName: # If this was initialised with Version(None) or Version("")
|
||||
self.components = tuple()
|
||||
self.status = str()
|
||||
elif isinstance(versionName, str):
|
||||
self.components, self.status = Version.toComponents(versionName)
|
||||
elif isinstance(versionName, (list, tuple)):
|
||||
self.components = tuple([int(v) for v in versionName])
|
||||
|
@ -278,36 +188,36 @@ def nodeVersion(nodeDesc, default=None):
|
|||
return moduleVersion(nodeDesc.__module__, default)
|
||||
|
||||
|
||||
def registerNodeType(nodeType):
|
||||
def registerNodeType(nodeType, module=None):
|
||||
""" Register a Node Type based on a Node Description class.
|
||||
|
||||
After registration, nodes of this type can be instantiated in a Graph.
|
||||
"""
|
||||
global nodesDesc
|
||||
if nodeType.__name__ in nodesDesc:
|
||||
logging.error("Node Desc {} is already registered.".format(nodeType.__name__))
|
||||
nodesDesc[nodeType.__name__] = nodeType
|
||||
# Register the node in plugin manager
|
||||
registered = pluginManager.registerNode(nodeType, module=module)
|
||||
|
||||
# The plugin was already registered
|
||||
if not registered:
|
||||
return
|
||||
|
||||
# Plugin Name
|
||||
name = nodeType.__name__
|
||||
|
||||
# Check the status of the plugin to identify if we have any errors on it while loading ?
|
||||
if pluginManager.status(name) == PluginStatus.ERRORED:
|
||||
errors = ", ".join(pluginManager.errors(name))
|
||||
logging.warning(f"[PluginManager] {name}: The following parameters do not have valid default values/ranges: {errors}.")
|
||||
|
||||
|
||||
def unregisterNodeType(nodeType):
|
||||
""" Remove 'nodeType' from the list of register node types. """
|
||||
global nodesDesc
|
||||
assert nodeType.__name__ in nodesDesc
|
||||
del nodesDesc[nodeType.__name__]
|
||||
|
||||
|
||||
def loadNodes(folder, packageName):
|
||||
return loadPlugins(folder, packageName, desc.Node)
|
||||
# Unregister the node from plugin manager
|
||||
pluginManager.unregisterNode(nodeType)
|
||||
|
||||
|
||||
def loadAllNodes(folder):
|
||||
global nodesDesc
|
||||
for importer, package, ispkg in pkgutil.walk_packages([folder]):
|
||||
if ispkg:
|
||||
nodeTypes = loadNodes(folder, package)
|
||||
for nodeType in nodeTypes:
|
||||
registerNodeType(nodeType)
|
||||
logging.debug('Nodes loaded [{}]: {}'.format(package, ', '.join([nodeType.__name__ for nodeType in nodeTypes])))
|
||||
# Load plugins from the node's plugin manager
|
||||
pluginManager.load(folder)
|
||||
|
||||
|
||||
def registerSubmitter(s):
|
||||
|
@ -318,7 +228,7 @@ def registerSubmitter(s):
|
|||
|
||||
|
||||
def loadSubmitters(folder, packageName):
|
||||
return loadPlugins(folder, packageName, BaseSubmitter)
|
||||
return _plugins.Pluginator.get(folder, packageName, BaseSubmitter)
|
||||
|
||||
|
||||
def loadPipelineTemplates(folder):
|
||||
|
|
|
@ -16,7 +16,7 @@ from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
|
|||
from meshroom.core import Version
|
||||
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
|
||||
from meshroom.core.exception import GraphCompatibilityError, StopGraphVisit, StopBranchVisit
|
||||
from meshroom.core.node import nodeFactory, Status, Node, CompatibilityNode
|
||||
from meshroom.core.node import nodeFactory, Status, Node, CompatibilityNode, IncompatiblePluginNode
|
||||
|
||||
# Replace default encoder to support Enums
|
||||
|
||||
|
@ -444,7 +444,7 @@ class Graph(BaseObject):
|
|||
# Second pass to update all the links in the input/output attributes for every node with the new names
|
||||
for nodeName, nodeData in updatedData.items():
|
||||
nodeType = nodeData.get("nodeType", None)
|
||||
nodeDesc = meshroom.core.nodesDesc[nodeType]
|
||||
nodeDesc = meshroom.core.pluginManager.descriptor(nodeType)
|
||||
|
||||
inputs = nodeData.get("inputs", {})
|
||||
outputs = nodeData.get("outputs", {})
|
||||
|
@ -762,7 +762,14 @@ class Graph(BaseObject):
|
|||
if name and name in self._nodes.keys():
|
||||
name = self._createUniqueNodeName(name)
|
||||
|
||||
n = self.addNode(Node(nodeType, position=position, **kwargs), uniqueName=name)
|
||||
# The Node Type to Construcct
|
||||
Type = Node
|
||||
|
||||
if meshroom.core.pluginManager.status(nodeType) == meshroom.core.PluginStatus.ERRORED:
|
||||
Type = IncompatiblePluginNode
|
||||
|
||||
# Construct the Node
|
||||
n = self.addNode(Type(nodeType, position=position, **kwargs), uniqueName=name)
|
||||
n.updateInternals()
|
||||
return n
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# coding:utf-8
|
||||
# Types
|
||||
from typing import List
|
||||
|
||||
import atexit
|
||||
import copy
|
||||
import datetime
|
||||
|
@ -495,9 +498,9 @@ class BaseNode(BaseObject):
|
|||
self._nodeType = nodeType
|
||||
self.nodeDesc = None
|
||||
|
||||
# instantiate node description if nodeType is valid
|
||||
if nodeType in meshroom.core.nodesDesc:
|
||||
self.nodeDesc = meshroom.core.nodesDesc[nodeType]()
|
||||
# instantiate node description if nodeType has been registered
|
||||
if meshroom.core.pluginManager.registered(nodeType):
|
||||
self.nodeDesc = meshroom.core.pluginManager.descriptor(nodeType)()
|
||||
|
||||
self.packageName = self.packageVersion = ""
|
||||
self._internalFolder = ""
|
||||
|
@ -1356,7 +1359,7 @@ class BaseNode(BaseObject):
|
|||
False otherwise.
|
||||
"""
|
||||
for attr in self._attributes:
|
||||
if attr.enabled and attr.isOutput and (attr.desc.semantic == "sequence" or
|
||||
if attr.enabled and attr.isOutput and (attr.desc.semantic == "sequence" or
|
||||
attr.desc.semantic == "imageList"):
|
||||
return True
|
||||
return False
|
||||
|
@ -1593,6 +1596,7 @@ class CompatibilityIssue(Enum):
|
|||
VersionConflict = 2 # mismatch between node's description version and serialized node data
|
||||
DescriptionConflict = 3 # mismatch between node's description attributes and serialized node data
|
||||
UidConflict = 4 # mismatch between computed UIDs and UIDs stored in serialized node data
|
||||
PluginIssue = 5 # Issue with interpreting the plugin due to any issues with interpreting the plugin
|
||||
|
||||
|
||||
class CompatibilityNode(BaseNode):
|
||||
|
@ -1754,8 +1758,9 @@ class CompatibilityNode(BaseNode):
|
|||
self._attributes.add(attribute)
|
||||
return matchDesc
|
||||
|
||||
@property
|
||||
def issueDetails(self):
|
||||
def _issueDetails(self) -> str:
|
||||
""" Returns Issue Details.
|
||||
"""
|
||||
if self.issue == CompatibilityIssue.UnknownNodeType:
|
||||
return "Unknown node type: '{}'.".format(self.nodeType)
|
||||
elif self.issue == CompatibilityIssue.VersionConflict:
|
||||
|
@ -1766,8 +1771,14 @@ class CompatibilityNode(BaseNode):
|
|||
return "Node attributes do not match node description."
|
||||
elif self.issue == CompatibilityIssue.UidConflict:
|
||||
return "Node UID differs from the expected one."
|
||||
else:
|
||||
return "Unknown error."
|
||||
elif self.issue == CompatibilityIssue.PluginIssue:
|
||||
return "Error interpreting the Node Plugin."
|
||||
|
||||
return "Unknown error."
|
||||
|
||||
@property
|
||||
def issueDetails(self) -> str:
|
||||
return self._issueDetails()
|
||||
|
||||
@property
|
||||
def inputs(self):
|
||||
|
@ -1852,6 +1863,74 @@ class CompatibilityNode(BaseNode):
|
|||
issueDetails = Property(str, issueDetails.fget, constant=True)
|
||||
|
||||
|
||||
class IncompatiblePluginNode(CompatibilityNode):
|
||||
""" Fallback BaseNode subclass to instantiate Nodes having compatibility issues with current type description.
|
||||
CompatibilityNode creates an 'empty-shell' exposing the deserialized node as-is,
|
||||
with all its inputs and precomputed outputs.
|
||||
"""
|
||||
|
||||
def __init__(self, nodeType, position=None, issue=CompatibilityIssue.PluginIssue, parent=None, **kwargs):
|
||||
super(IncompatiblePluginNode, self).__init__(nodeType, {}, position=position, issue=issue, parent=parent)
|
||||
|
||||
self.packageName = self.nodeDesc.packageName
|
||||
self.packageVersion = self.nodeDesc.packageVersion
|
||||
self._internalFolder = self.nodeDesc.internalFolder
|
||||
|
||||
# Fetch the errors for the plugin
|
||||
self._nodeErrors = self.nodeDesc.plugin.errors
|
||||
|
||||
# Add the Attributes
|
||||
for attrDesc in self.nodeDesc.inputs:
|
||||
# Don't add any invalid attributes
|
||||
if attrDesc.invalid:
|
||||
continue
|
||||
|
||||
self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=False, node=self))
|
||||
|
||||
for attrDesc in self.nodeDesc.outputs:
|
||||
# Don't add any invalid attributes
|
||||
if attrDesc.invalid:
|
||||
continue
|
||||
|
||||
self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=True, node=self))
|
||||
|
||||
for attrDesc in self.nodeDesc.internalInputs:
|
||||
# Don't add any invalid attributes
|
||||
if attrDesc.invalid:
|
||||
continue
|
||||
|
||||
self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=False,
|
||||
node=self))
|
||||
|
||||
@property
|
||||
def issueDetails(self) -> str:
|
||||
""" Returns any issue details for the Node.
|
||||
"""
|
||||
# The basic issue detail
|
||||
details = [self._issueDetails()]
|
||||
|
||||
# Add the Node Parametric error details
|
||||
details.extend(self._errorDetails())
|
||||
|
||||
return "\n".join(details)
|
||||
|
||||
# Protected
|
||||
def _errorDetails(self) -> List[str]:
|
||||
""" Returns the details of the Parametric errors on the Node.
|
||||
"""
|
||||
errors = ["Following parameters have invalid default values/ranges:"]
|
||||
|
||||
# Add the parameters from the node Errors
|
||||
errors.extend([f"* Param {param}" for param in self._nodeErrors])
|
||||
|
||||
return errors
|
||||
|
||||
# Properties
|
||||
# An Incompatible Plguin Node should not be upgraded but only reloaded
|
||||
canUpgrade = Property(bool, lambda _: False, constant=True)
|
||||
issueDetails = Property(str, issueDetails.fget, constant=True)
|
||||
|
||||
|
||||
def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
|
||||
"""
|
||||
Create a node instance by deserializing the given node data.
|
||||
|
@ -1885,11 +1964,11 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
|
|||
|
||||
compatibilityIssue = None
|
||||
|
||||
nodeDesc = None
|
||||
try:
|
||||
nodeDesc = meshroom.core.nodesDesc[nodeType]
|
||||
except KeyError:
|
||||
# Unknown node type
|
||||
# Returns the desc.Node inherited class or None if the plugin was not registered
|
||||
nodeDesc = meshroom.core.pluginManager.descriptor(nodeType)
|
||||
|
||||
# Node plugin was not registered
|
||||
if not nodeDesc:
|
||||
compatibilityIssue = CompatibilityIssue.UnknownNodeType
|
||||
|
||||
# Unknown node type should take precedence over UID conflict, as it cannot be resolved
|
||||
|
@ -1909,7 +1988,7 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
|
|||
# do not perform that check for internal attributes because there is no point in
|
||||
# raising compatibility issues if their number differs: in that case, it is only useful
|
||||
# if some internal attributes do not exist or are invalid
|
||||
if not template and (sorted([attr.name for attr in nodeDesc.inputs
|
||||
if not template and (sorted([attr.name for attr in nodeDesc.inputs
|
||||
if not isinstance(attr, desc.PushButtonParam)]) != sorted(inputs.keys()) or
|
||||
sorted([attr.name for attr in nodeDesc.outputs if not attr.isDynamicValue]) !=
|
||||
sorted(outputs.keys())):
|
||||
|
|
|
@ -11,7 +11,7 @@ from PySide6.QtGui import QIcon
|
|||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import meshroom
|
||||
from meshroom.core import nodesDesc
|
||||
from meshroom.core import pluginManager
|
||||
from meshroom.core.taskManager import TaskManager
|
||||
from meshroom.common import Property, Variant, Signal, Slot
|
||||
|
||||
|
@ -240,6 +240,7 @@ class MeshroomApp(QApplication):
|
|||
components.registerTypes()
|
||||
|
||||
# expose available node types that can be instantiated
|
||||
nodesDesc = pluginManager.descriptors
|
||||
self.engine.rootContext().setContextProperty("_nodeTypes", {n: {"category": nodesDesc[n].category} for n in sorted(nodesDesc.keys())})
|
||||
|
||||
# instantiate Reconstruction object
|
||||
|
|
|
@ -496,7 +496,8 @@ class Reconstruction(UIGraph):
|
|||
# Create all possible entries
|
||||
for category, _ in self.activeNodeCategories.items():
|
||||
self._activeNodes.add(ActiveNode(category, parent=self))
|
||||
for nodeType, _ in meshroom.core.nodesDesc.items():
|
||||
|
||||
for nodeType in meshroom.core.pluginManager.descriptors:
|
||||
self._activeNodes.add(ActiveNode(nodeType, parent=self))
|
||||
|
||||
def clearActiveNodes(self):
|
||||
|
@ -648,7 +649,9 @@ class Reconstruction(UIGraph):
|
|||
if not sfmFile or not os.path.isfile(sfmFile):
|
||||
self.tempCameraInit = None
|
||||
return
|
||||
nodeDesc = meshroom.core.nodesDesc["CameraInit"]()
|
||||
|
||||
# The camera init node should always exist
|
||||
nodeDesc = meshroom.core.pluginManager.descriptor("CameraInit")()
|
||||
views, intrinsics = nodeDesc.readSfMData(sfmFile)
|
||||
tmpCameraInit = Node("CameraInit", viewpoints=views, intrinsics=intrinsics)
|
||||
tmpCameraInit.locked = True
|
||||
|
|
|
@ -197,7 +197,7 @@ def test_description_conflict():
|
|||
Test compatibility behavior for conflicting node descriptions.
|
||||
"""
|
||||
# copy registered node types to be able to restore them
|
||||
originalNodeTypes = copy.copy(meshroom.core.nodesDesc)
|
||||
originalNodeTypes = copy.copy(meshroom.core.pluginManager.descriptors)
|
||||
|
||||
nodeTypes = [SampleNodeV1, SampleNodeV2, SampleNodeV3, SampleNodeV4, SampleNodeV5]
|
||||
nodes = []
|
||||
|
@ -224,7 +224,7 @@ def test_description_conflict():
|
|||
# offset node types register to create description conflicts
|
||||
# each node type name now reference the next one's implementation
|
||||
for i, nt in enumerate(nodeTypes[:-1]):
|
||||
meshroom.core.nodesDesc[nt.__name__] = nodeTypes[i+1]
|
||||
meshroom.core.pluginManager.register(nt.__name__, nodeTypes[i+1])
|
||||
|
||||
# reload file
|
||||
g = loadGraph(graphFile)
|
||||
|
@ -306,7 +306,7 @@ def test_description_conflict():
|
|||
raise ValueError("Unexpected node type: " + srcNode.nodeType)
|
||||
|
||||
# restore original node types
|
||||
meshroom.core.nodesDesc = originalNodeTypes
|
||||
meshroom.core.pluginManager._descriptors = originalNodeTypes # pylint: disable=protected-access
|
||||
|
||||
|
||||
def test_upgradeAllNodes():
|
||||
|
@ -331,8 +331,8 @@ def test_upgradeAllNodes():
|
|||
unregisterNodeType(SampleNodeV2)
|
||||
unregisterNodeType(SampleInputNodeV2)
|
||||
# replace SampleNodeV1 by SampleNodeV2 and SampleInputNodeV1 by SampleInputNodeV2
|
||||
meshroom.core.nodesDesc[SampleNodeV1.__name__] = SampleNodeV2
|
||||
meshroom.core.nodesDesc[SampleInputNodeV1.__name__] = SampleInputNodeV2
|
||||
meshroom.core.pluginManager.register(SampleNodeV1.__name__, SampleNodeV2)
|
||||
meshroom.core.pluginManager.register(SampleInputNodeV1.__name__, SampleInputNodeV2)
|
||||
|
||||
# reload file
|
||||
g = loadGraph(graphFile)
|
||||
|
@ -369,7 +369,7 @@ def test_conformUpgrade():
|
|||
g.save(graphFile)
|
||||
|
||||
# replace SampleNodeV5 by SampleNodeV6
|
||||
meshroom.core.nodesDesc[SampleNodeV5.__name__] = SampleNodeV6
|
||||
meshroom.core.pluginManager.register(SampleNodeV5.__name__, SampleNodeV6)
|
||||
|
||||
# reload file
|
||||
g = loadGraph(graphFile)
|
||||
|
|
67
tests/test_plugins.py
Normal file
67
tests/test_plugins.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
""" Test for Meshroom Plugins.
|
||||
"""
|
||||
#!/usr/bin/env python
|
||||
# coding:utf-8
|
||||
|
||||
from meshroom.core import _plugins
|
||||
from meshroom.core import desc, registerNodeType, unregisterNodeType
|
||||
|
||||
|
||||
class SampleNode(desc.Node):
|
||||
""" Sample Node for unit testing """
|
||||
|
||||
category = "Sample"
|
||||
|
||||
inputs = [
|
||||
desc.File(name='input', label='Input', description='', value='',),
|
||||
desc.StringParam(name='paramA', label='ParamA', description='', value='', invalidate=False) # No impact on UID
|
||||
]
|
||||
outputs = [
|
||||
desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder)
|
||||
]
|
||||
|
||||
|
||||
def test_plugin_management():
|
||||
""" Tests the plugin manager for registering and unregistering node.
|
||||
"""
|
||||
# Sample Node name
|
||||
name = SampleNode.__name__
|
||||
|
||||
# Register the node
|
||||
registerNodeType(SampleNode)
|
||||
|
||||
# Since the Node Plugin Manager is a singleton instance
|
||||
# We should still be able to instantiate and have a look at out registered plugins directly
|
||||
pluginManager = _plugins.NodePluginManager()
|
||||
|
||||
# Assert that the plugin we have registered above is indeed registered
|
||||
assert pluginManager.registered(name)
|
||||
|
||||
# Assert that the plugin can only be registered once
|
||||
assert not pluginManager.registerNode(SampleNode)
|
||||
|
||||
# And once un-registered, it should no longer be present in the pluginManager
|
||||
unregisterNodeType(SampleNode)
|
||||
|
||||
# Assert that the plugin we have registered above is indeed registered
|
||||
assert not pluginManager.registered(name)
|
||||
assert name not in pluginManager.descriptors
|
||||
|
||||
def test_descriptor():
|
||||
""" Tests the Descriptor and NodeDescriptor instances.
|
||||
"""
|
||||
# Register the node
|
||||
registerNodeType(SampleNode)
|
||||
|
||||
# Since the Node Plugin Manager is a singleton instance
|
||||
# We should still be able to instantiate and have a look at out registered plugins directly
|
||||
pluginManager = _plugins.NodePluginManager()
|
||||
|
||||
# Assert the descriptor is same as the Plugin NodeType
|
||||
assert pluginManager.descriptor(SampleNode.__name__).__name__ == SampleNode.__name__
|
||||
|
||||
# Assert that the category of the NodeDescriptor is correct for the registered plugin
|
||||
assert pluginManager.descriptors.get(SampleNode.__name__).category == "Sample"
|
||||
|
||||
# Finally unregister the plugin
|
||||
unregisterNodeType(SampleNode)
|
|
@ -34,9 +34,10 @@ def test_templateVersions():
|
|||
|
||||
for _, nodeData in graphData.items():
|
||||
nodeType = nodeData["nodeType"]
|
||||
assert nodeType in meshroom.core.nodesDesc
|
||||
# Assert that the plugin (nodeType) is indeed registered to be used
|
||||
assert meshroom.core.pluginManager.registered(nodeType)
|
||||
|
||||
nodeDesc = meshroom.core.nodesDesc[nodeType]
|
||||
nodeDesc = meshroom.core.pluginManager.descriptor(nodeType)
|
||||
currentNodeVersion = meshroom.core.nodeVersion(nodeDesc)
|
||||
|
||||
inputs = nodeData.get("inputs", {})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue