[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:
waaake 2024-11-10 12:05:20 +01:00
parent 648b0950b8
commit 02b85cb273
11 changed files with 600 additions and 149 deletions

View 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
View 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
View 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])))

View file

@ -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):

View file

@ -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

View file

@ -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())):

View file

@ -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

View file

@ -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

View file

@ -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
View 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)

View file

@ -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", {})