Skip to content

Commit 64f806b

Browse files
authored
Merge pull request #2733 from alicevision/dev/pluginsSupport
New Plugin Architecture for Node Registration
2 parents a50282d + d3738fb commit 64f806b

34 files changed

+1393
-464
lines changed

meshroom/common/qt.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,8 @@ def _dereferenceItem(self, item):
305305
key = getattr(item, self._keyAttrName, None)
306306
if key is None:
307307
return
308-
assert key in self._objectByKey
308+
if key not in self._objectByKey:
309+
raise RuntimeError(f"{key} is not in the Model: {self._objectByKey.keys()}")
309310
del self._objectByKey[key]
310311

311312
def onRequestDeletion(self, item):

meshroom/core/__init__.py

Lines changed: 97 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
except Exception:
2020
pass
2121

22+
from meshroom.core.plugins import NodePlugin, NodePluginManager, Plugin, ProcessEnv
2223
from meshroom.core.submitter import BaseSubmitter
2324
from meshroom.env import EnvVar, meshroomFolder
2425
from . import desc
@@ -31,7 +32,7 @@
3132
sessionUid = str(uuid.uuid1())
3233

3334
cacheFolderName = 'MeshroomCache'
34-
nodesDesc: dict[str, desc.BaseNode] = {}
35+
pluginManager: NodePluginManager = NodePluginManager()
3536
submitters: dict[str, BaseSubmitter] = {}
3637
pipelineTemplates: dict[str, str] = {}
3738

@@ -41,7 +42,6 @@ def hashValue(value) -> str:
4142
hashObject = hashlib.sha1(str(value).encode('utf-8'))
4243
return hashObject.hexdigest()
4344

44-
4545
@contextmanager
4646
def add_to_path(p):
4747
import sys
@@ -53,9 +53,15 @@ def add_to_path(p):
5353
finally:
5454
sys.path = old_path
5555

56-
57-
def loadClasses(folder, packageName, classType):
56+
def loadClasses(folder: str, packageName: str, classType: type) -> list[type]:
5857
"""
58+
Go over the Python module named "packageName" located in "folder" to find files
59+
that contain classes of type "classType" and return these classes in a list.
60+
61+
Args:
62+
folder: the folder to load the module from.
63+
packageName: the name of the module to look for nodes in.
64+
classType: the class to look for in the files that are inspected.
5965
"""
6066
classes = []
6167
errors = []
@@ -67,7 +73,8 @@ def loadClasses(folder, packageName, classType):
6773

6874
try:
6975
package = importlib.import_module(packageName)
70-
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
76+
packageName = package.packageName if hasattr(package, "packageName") \
77+
else package.__name__
7178
packageVersion = getattr(package, "__version__", None)
7279
packagePath = os.path.dirname(package.__file__)
7380
except Exception as e:
@@ -83,31 +90,34 @@ def loadClasses(folder, packageName, classType):
8390
)
8491
return []
8592

86-
for importer, pluginName, ispkg in pkgutil.iter_modules(package.__path__):
87-
pluginModuleName = '.' + pluginName
93+
for _, pluginName, _ in pkgutil.iter_modules(package.__path__):
94+
pluginModuleName = "." + pluginName
8895

8996
try:
9097
pluginMod = importlib.import_module(pluginModuleName, package=package.__name__)
91-
plugins = [plugin for name, plugin in inspect.getmembers(pluginMod, inspect.isclass)
92-
if plugin.__module__ == f'{package.__name__}.{pluginName}'
98+
plugins = [plugin for _, plugin in inspect.getmembers(pluginMod, inspect.isclass)
99+
if plugin.__module__ == f"{package.__name__}.{pluginName}"
93100
and issubclass(plugin, classType)]
101+
94102
if not plugins:
95-
logging.warning(f"No class defined in plugin: {pluginModuleName}")
103+
# Only packages/folders have __path__, single module/file do not have it.
104+
isPackage = hasattr(pluginMod, "__path__")
105+
# Sub-folders/Packages should not raise a warning
106+
if not isPackage:
107+
logging.warning(f"No class defined in plugin: {package.__name__}.{pluginName} ('{pluginMod.__file__}')")
96108

97-
importPlugin = True
98109
for p in plugins:
99-
if classType == desc.Node:
100-
nodeErrors = validateNodeDesc(p)
101-
if nodeErrors:
102-
errors.append(" * {}: The following parameters do not have valid default values/ranges: {}"
103-
.format(pluginName, ", ".join(nodeErrors)))
104-
importPlugin = False
105-
break
106110
p.packageName = packageName
107111
p.packageVersion = packageVersion
108112
p.packagePath = packagePath
109-
if importPlugin:
110-
classes.extend(plugins)
113+
if classType == desc.BaseNode:
114+
nodePlugin = NodePlugin(p)
115+
if nodePlugin.errors:
116+
errors.append(" * {}: The following parameters do not have valid " \
117+
"default values/ranges: {}".format(pluginName, ", ".join(nodePlugin.errors)))
118+
classes.append(nodePlugin)
119+
else:
120+
classes.append(p)
111121
except Exception as e:
112122
tb = traceback.extract_tb(e.__traceback__)
113123
last_call = tb[-1]
@@ -124,41 +134,42 @@ def loadClasses(folder, packageName, classType):
124134
logging.warning(' The following "{package}" plugins could not be loaded:\n'
125135
'{errorMsg}\n'
126136
.format(package=packageName, errorMsg='\n'.join(errors)))
127-
return classes
128137

138+
return classes
129139

130-
def validateNodeDesc(nodeDesc):
140+
def loadClassesNodes(folder: str, packageName: str) -> list[NodePlugin]:
131141
"""
132-
Check that the node has a valid description before being loaded. For the description
133-
to be valid, the default value of every parameter needs to correspond to the type
134-
of the parameter.
135-
An empty returned list means that every parameter is valid, and so is the node's description.
136-
If it is not valid, the returned list contains the names of the invalid parameters. In case
137-
of nested parameters (parameters in groups or lists, for example), the name of the parameter
138-
follows the name of the parent attributes. For example, if the attribute "x", contained in group
139-
"group", is invalid, then it will be added to the list as "group:x".
142+
Return the list of all the NodePlugins that were created following the search of the
143+
Python module named "packageName" located in the folder "folder".
144+
A NodePlugin is created when a file within "packageName" that contains a class inheriting
145+
desc.BaseNode is found.
140146
141147
Args:
142-
nodeDesc (desc.Node): description of the node
148+
folder: the folder to load the module from.
149+
packageName: the name of the module to look for nodes in.
143150
144151
Returns:
145-
errors (list): the list of invalid parameters if there are any, empty list otherwise
152+
list[NodePlugin]: a list of all the NodePlugins that were created based on the
153+
module's search. If none has been created, an empty list is returned.
146154
"""
147-
errors = []
155+
return loadClasses(folder, packageName, desc.BaseNode)
148156

149-
for param in nodeDesc.inputs:
150-
err = param.checkValueTypes()
151-
if err:
152-
errors.append(err)
157+
def loadClassesSubmitters(folder: str, packageName: str) -> list[BaseSubmitter]:
158+
"""
159+
Return the list of all the submitters that were found during the search of the
160+
Python module named "packageName" that located in the folder "folder".
161+
A submitter is found if a file within "packageName" contains a class inheriting
162+
from BaseSubmitter.
153163
154-
for param in nodeDesc.outputs:
155-
if param.value is None:
156-
continue
157-
err = param.checkValueTypes()
158-
if err:
159-
errors.append(err)
164+
Args:
165+
folder: the folder to load the module from.
166+
packageName: the name of the module to look for nodes in.
160167
161-
return errors
168+
Returns:
169+
list[BaseSubmitter]: a list of all the submitters that were found during the
170+
module's search
171+
"""
172+
return loadClasses(folder, packageName, BaseSubmitter)
162173

163174

164175
class Version:
@@ -250,7 +261,8 @@ def toComponents(versionName):
250261
status = ''
251262
# If there is a status, it is placed after a "-"
252263
splitComponents = versionName.split("-", maxsplit=1)
253-
if (len(splitComponents) > 1): # If there is no status, splitComponents is equal to [versionName]
264+
# If there is no status, splitComponents is equal to [versionName]
265+
if len(splitComponents) > 1:
254266
status = splitComponents[-1]
255267
return tuple([int(v) for v in splitComponents[0].split(".")]), status
256268

@@ -279,7 +291,7 @@ def micro(self):
279291
return self.components[2]
280292

281293

282-
def moduleVersion(moduleName, default=None):
294+
def moduleVersion(moduleName: str, default=None):
283295
""" Return the version of a module indicated with '__version__' keyword.
284296
285297
Args:
@@ -292,7 +304,7 @@ def moduleVersion(moduleName, default=None):
292304
return getattr(sys.modules[moduleName], "__version__", default)
293305

294306

295-
def nodeVersion(nodeDesc, default=None):
307+
def nodeVersion(nodeDesc: desc.Node, default=None):
296308
""" Return node type version for the given node description class.
297309
298310
Args:
@@ -305,38 +317,28 @@ def nodeVersion(nodeDesc, default=None):
305317
return moduleVersion(nodeDesc.__module__, default)
306318

307319

308-
def registerNodeType(nodeType):
309-
""" Register a Node Type based on a Node Description class.
310-
311-
After registration, nodes of this type can be instantiated in a Graph.
312-
"""
313-
if nodeType.__name__ in nodesDesc:
314-
logging.error(f"Node Desc {nodeType.__name__} is already registered.")
315-
nodesDesc[nodeType.__name__] = nodeType
316-
317-
318-
def unregisterNodeType(nodeType):
319-
""" Remove 'nodeType' from the list of register node types. """
320-
assert nodeType.__name__ in nodesDesc
321-
del nodesDesc[nodeType.__name__]
322-
323-
324-
def loadNodes(folder, packageName):
320+
def loadNodes(folder, packageName) -> list[NodePlugin]:
325321
if not os.path.isdir(folder):
326322
logging.error(f"Node folder '{folder}' does not exist.")
327-
return
323+
return []
328324

329-
return loadClasses(folder, packageName, desc.BaseNode)
325+
nodes = loadClassesNodes(folder, packageName)
326+
return nodes
330327

331328

332-
def loadAllNodes(folder):
333-
for importer, package, ispkg in pkgutil.walk_packages([folder]):
329+
def loadAllNodes(folder) -> list[Plugin]:
330+
plugins = []
331+
for _, package, ispkg in pkgutil.iter_modules([folder]):
334332
if ispkg:
335-
nodeTypes = loadNodes(folder, package)
336-
for nodeType in nodeTypes:
337-
registerNodeType(nodeType)
338-
nodesStr = ', '.join([nodeType.__name__ for nodeType in nodeTypes])
339-
logging.debug(f'Nodes loaded [{package}]: {nodesStr}')
333+
plugin = Plugin(package, folder)
334+
nodePlugins = loadNodes(folder, package)
335+
if nodePlugins:
336+
for node in nodePlugins:
337+
plugin.addNodePlugin(node)
338+
nodesStr = ', '.join([node.nodeDescriptor.__name__ for node in nodePlugins])
339+
logging.debug(f'Nodes loaded [{package}]: {nodesStr}')
340+
plugins.append(plugin)
341+
return plugins
340342

341343

342344
def loadPluginFolder(folder):
@@ -349,26 +351,29 @@ def loadPluginFolder(folder):
349351
logging.info(f"Plugin folder '{folder}' does not contain a 'meshroom' folder.")
350352
return
351353

352-
binFolders = [Path(folder, 'bin')]
353-
libFolders = [Path(folder, 'lib'), Path(folder, 'lib64')]
354-
pythonPathFolders = [Path(folder)] + binFolders
354+
processEnv = ProcessEnv(folder)
355+
356+
plugins = loadAllNodes(folder=mrFolder)
357+
if plugins:
358+
for plugin in plugins:
359+
pluginManager.addPlugin(plugin)
360+
pipelineTemplates.update(plugin.templates)
355361

356-
loadAllNodes(folder=mrFolder)
357-
loadPipelineTemplates(folder=mrFolder)
362+
return plugins
358363

359364

360365
def loadPluginsFolder(folder):
361366
if not os.path.isdir(folder):
362367
logging.debug(f"PluginSet folder '{folder}' does not exist.")
363368
return
364-
369+
365370
for file in os.listdir(folder):
366371
if os.path.isdir(file):
367372
subFolder = os.path.join(folder, file)
368373
loadPluginFolder(subFolder)
369374

370375

371-
def registerSubmitter(s):
376+
def registerSubmitter(s: BaseSubmitter):
372377
if s.name in submitters:
373378
logging.error(f"Submitter {s.name} is already registered.")
374379
submitters[s.name] = s
@@ -379,30 +384,31 @@ def loadSubmitters(folder, packageName):
379384
logging.error(f"Submitters folder '{folder}' does not exist.")
380385
return
381386

382-
return loadClasses(folder, packageName, BaseSubmitter)
383-
387+
return loadClassesSubmitters(folder, packageName)
384388

385-
def loadPipelineTemplates(folder):
389+
def loadPipelineTemplates(folder: str):
386390
if not os.path.isdir(folder):
387391
logging.error(f"Pipeline templates folder '{folder}' does not exist.")
388392
return
389393
for file in os.listdir(folder):
390394
if file.endswith(".mg") and file not in pipelineTemplates:
391395
pipelineTemplates[os.path.splitext(file)[0]] = os.path.join(folder, file)
392396

393-
394397
def initNodes():
395398
additionalNodesPath = EnvVar.getList(EnvVar.MESHROOM_NODES_PATH)
396-
nodesFolders = [os.path.join(meshroomFolder, 'nodes')] + additionalNodesPath
399+
nodesFolders = [os.path.join(meshroomFolder, "nodes")] + additionalNodesPath
397400
for f in nodesFolders:
398-
loadAllNodes(folder=f)
401+
plugins = loadAllNodes(folder=f)
402+
if plugins:
403+
for plugin in plugins:
404+
pluginManager.addPlugin(plugin)
399405

400406

401407
def initSubmitters():
402408
additionalPaths = EnvVar.getList(EnvVar.MESHROOM_SUBMITTERS_PATH)
403409
allSubmittersFolders = [meshroomFolder] + additionalPaths
404410
for folder in allSubmittersFolders:
405-
subs = loadSubmitters(folder, 'submitters')
411+
subs = loadSubmitters(folder, "submitters")
406412
for sub in subs:
407413
registerSubmitter(sub())
408414

@@ -411,13 +417,15 @@ def initPipelines():
411417
# Load pipeline templates: check in the default folder and any folder the user might have
412418
# added to the environment variable
413419
additionalPipelinesPath = EnvVar.getList(EnvVar.MESHROOM_PIPELINE_TEMPLATES_PATH)
414-
pipelineTemplatesFolders = [os.path.join(meshroomFolder, 'pipelines')] + additionalPipelinesPath
420+
pipelineTemplatesFolders = [os.path.join(meshroomFolder, "pipelines")] + additionalPipelinesPath
415421
for f in pipelineTemplatesFolders:
416422
loadPipelineTemplates(f)
423+
for plugin in pluginManager.getPlugins().values():
424+
pipelineTemplates.update(plugin.templates)
417425

418426

419427
def initPlugins():
420428
additionalpluginsPath = EnvVar.getList(EnvVar.MESHROOM_PLUGINS_PATH)
421-
nodesFolders = [os.path.join(meshroomFolder, 'plugins')] + additionalpluginsPath
429+
nodesFolders = [os.path.join(meshroomFolder, "plugins")] + additionalpluginsPath
422430
for f in nodesFolders:
423431
loadPluginFolder(folder=f)

0 commit comments

Comments
 (0)