Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b564203
[core] Add `ProcessEnv` class in new `plugins` module
cbentejac May 5, 2025
6f69588
[core] Add typing on methods
cbentejac May 6, 2025
f669402
[core] plugins: Add a `Plugin` class that contains a set of nodes
cbentejac May 7, 2025
1531e11
[core] plugins: Add `NodePluginStatus` enum for `NodePlugin` objects
cbentejac May 7, 2025
fa3cb8c
[core] plugins: Move `validateNodeDesc` from __init__.py to plugins.py
cbentejac May 7, 2025
63e01d4
[core] plugins: Add working `NodePlugin` class to represent nodes
cbentejac May 7, 2025
836a351
[core] plugins: Add a `NodePluginManager` class
cbentejac May 7, 2025
56eebe4
[core] plugins: Add templates handling in the `Plugin` class
cbentejac May 9, 2025
15c38b6
[core] Instantiate a `NodePluginManager` in the core module
cbentejac May 9, 2025
4a238b9
[core] Load and register nodes using the `NodePluginManager`
cbentejac May 9, 2025
88bee35
[core] Replace `nodesDesc` with the `NodePluginManager` instance
cbentejac May 12, 2025
441ba37
[core] Remove `un/registerNodeType` methods
cbentejac May 12, 2025
28042dd
[tests] Harmonize and clean-up syntax across test files
cbentejac May 12, 2025
777ed42
[tests] Use the `NodePluginManager` instance in the unit tests
cbentejac May 12, 2025
c16b56c
Clean-up: PEP8 linting and quotes harmonization
cbentejac May 22, 2025
98d90da
[core] plugins: Rename `getNodePlugin...` to `getRegisteredNodePlugin…
cbentejac May 22, 2025
a8a54f6
[core] plugins: Support finding unregistered nodes in plugins
cbentejac May 22, 2025
0adb375
[core] Add `PluginIssue` compatibility issue
cbentejac May 22, 2025
b3ee2ad
[core] plugins: Add new methods to the manager to manipulate plugins
cbentejac May 26, 2025
3af5acf
Plugins: Use simplified load/register of plugins and nodes when possible
cbentejac May 26, 2025
5bc09c8
[core] plugins: Add a method to reload a `NodePlugin`
cbentejac May 26, 2025
aa4d9ad
[tests] Add initial set of unit tests for plugins
cbentejac May 27, 2025
424abbf
[core] plugins: Check `NodePlugin`'s timestamp before reloading it
cbentejac Jun 4, 2025
3c57afb
[tests] Simplify registration/unregistration of nodes in tests
cbentejac Jun 4, 2025
a19c306
[core] plugins: Remove `register/unregisterPlugin` methods
cbentejac Jun 4, 2025
91e753c
[core] plugins: Handle corner cases when reloading nodes
cbentejac Jun 5, 2025
3d36ea5
[core] graph: Add a `reloadAllNodes` method to reload the current graph
cbentejac Jun 5, 2025
0c5e769
[ui] Add a "Reload All Nodes" menu to reload all the plugins' nodes
cbentejac Jun 5, 2025
98fbfae
[tests] Plugins: Add a `sleep` between file rewrites
cbentejac Jun 5, 2025
a6d80b3
[ui] Application: Add shortcut for the "Reload All Nodes" menu
cbentejac Jun 5, 2025
24e1457
[core] graph: Ensure all nodes are looped over in `reloadAllNodes`
cbentejac Jun 6, 2025
c58cf99
[core] plugins: `reload`: Return a bool depending on the reloading st…
cbentejac Jun 6, 2025
8349814
`reloadAllNodes`: Save reloaded types and only replace targeted nodes
cbentejac Jun 6, 2025
fc62ef7
[core/common] raise an error instead of an assert if the key does not…
fabiencastan Jun 7, 2025
2ad431a
[core] Node upgrade: check if the attributes exist
fabiencastan Jun 7, 2025
e0ba373
Minor renaming
fabiencastan Jun 7, 2025
9cffabb
[core] load all modules but not recursively
fabiencastan Jun 7, 2025
9dc0aca
[core] Adjust message when there is no class in the module
fabiencastan Jun 7, 2025
d3738fb
Disable too verbose debug message
fabiencastan Jun 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion meshroom/common/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@
key = getattr(item, self._keyAttrName, None)
if key is None:
return
assert key in self._objectByKey
if key not in self._objectByKey:
raise RuntimeError(f"{key} is not in the Model: {self._objectByKey.keys()}")

Check warning on line 309 in meshroom/common/qt.py

View check run for this annotation

Codecov / codecov/patch

meshroom/common/qt.py#L309

Added line #L309 was not covered by tests
del self._objectByKey[key]

def onRequestDeletion(self, item):
Expand Down
186 changes: 97 additions & 89 deletions meshroom/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
except Exception:
pass

from meshroom.core.plugins import NodePlugin, NodePluginManager, Plugin, ProcessEnv
from meshroom.core.submitter import BaseSubmitter
from meshroom.env import EnvVar, meshroomFolder
from . import desc
Expand All @@ -31,7 +32,7 @@
sessionUid = str(uuid.uuid1())

cacheFolderName = 'MeshroomCache'
nodesDesc: dict[str, desc.BaseNode] = {}
pluginManager: NodePluginManager = NodePluginManager()
submitters: dict[str, BaseSubmitter] = {}
pipelineTemplates: dict[str, str] = {}

Expand All @@ -41,7 +42,6 @@
hashObject = hashlib.sha1(str(value).encode('utf-8'))
return hashObject.hexdigest()


@contextmanager
def add_to_path(p):
import sys
Expand All @@ -53,9 +53,15 @@
finally:
sys.path = old_path


def loadClasses(folder, packageName, classType):
def loadClasses(folder: str, packageName: str, classType: type) -> list[type]:
"""
Go over the Python module named "packageName" located in "folder" to find files
that contain classes of type "classType" and return these classes in a list.

Args:
folder: the folder to load the module from.
packageName: the name of the module to look for nodes in.
classType: the class to look for in the files that are inspected.
"""
classes = []
errors = []
Expand All @@ -67,7 +73,8 @@

try:
package = importlib.import_module(packageName)
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
packageName = package.packageName if hasattr(package, "packageName") \
else package.__name__
packageVersion = getattr(package, "__version__", None)
packagePath = os.path.dirname(package.__file__)
except Exception as e:
Expand All @@ -83,31 +90,34 @@
)
return []

for importer, pluginName, ispkg in pkgutil.iter_modules(package.__path__):
pluginModuleName = '.' + pluginName
for _, pluginName, _ 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__ == f'{package.__name__}.{pluginName}'
plugins = [plugin for _, plugin in inspect.getmembers(pluginMod, inspect.isclass)
if plugin.__module__ == f"{package.__name__}.{pluginName}"
and issubclass(plugin, classType)]

if not plugins:
logging.warning(f"No class defined in plugin: {pluginModuleName}")
# Only packages/folders have __path__, single module/file do not have it.
isPackage = hasattr(pluginMod, "__path__")

Check warning on line 104 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L104

Added line #L104 was not covered by tests
# Sub-folders/Packages should not raise a warning
if not isPackage:
logging.warning(f"No class defined in plugin: {package.__name__}.{pluginName} ('{pluginMod.__file__}')")

Check warning on line 107 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L106-L107

Added lines #L106 - L107 were not covered by tests

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
p.packagePath = packagePath

Check notice on line 112 in meshroom/core/__init__.py

View check run for this annotation

codefactor.io / CodeFactor

meshroom/core/__init__.py#L112

The backslash is redundant between brackets. (E502)
if importPlugin:
classes.extend(plugins)
if classType == desc.BaseNode:
nodePlugin = NodePlugin(p)
if nodePlugin.errors:
errors.append(" * {}: The following parameters do not have valid " \
"default values/ranges: {}".format(pluginName, ", ".join(nodePlugin.errors)))
classes.append(nodePlugin)
else:
classes.append(p)

Check warning on line 120 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L120

Added line #L120 was not covered by tests
except Exception as e:
tb = traceback.extract_tb(e.__traceback__)
last_call = tb[-1]
Expand All @@ -124,41 +134,42 @@
logging.warning(' The following "{package}" plugins could not be loaded:\n'
'{errorMsg}\n'
.format(package=packageName, errorMsg='\n'.join(errors)))
return classes

return classes

def validateNodeDesc(nodeDesc):
def loadClassesNodes(folder: str, packageName: str) -> list[NodePlugin]:
"""
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".
Return the list of all the NodePlugins that were created following the search of the
Python module named "packageName" located in the folder "folder".
A NodePlugin is created when a file within "packageName" that contains a class inheriting
desc.BaseNode is found.

Args:
nodeDesc (desc.Node): description of the node
folder: the folder to load the module from.
packageName: the name of the module to look for nodes in.

Returns:
errors (list): the list of invalid parameters if there are any, empty list otherwise
list[NodePlugin]: a list of all the NodePlugins that were created based on the
module's search. If none has been created, an empty list is returned.
"""
errors = []
return loadClasses(folder, packageName, desc.BaseNode)

for param in nodeDesc.inputs:
err = param.checkValueTypes()
if err:
errors.append(err)
def loadClassesSubmitters(folder: str, packageName: str) -> list[BaseSubmitter]:
"""
Return the list of all the submitters that were found during the search of the
Python module named "packageName" that located in the folder "folder".
A submitter is found if a file within "packageName" contains a class inheriting
from BaseSubmitter.

for param in nodeDesc.outputs:
if param.value is None:
continue
err = param.checkValueTypes()
if err:
errors.append(err)
Args:
folder: the folder to load the module from.
packageName: the name of the module to look for nodes in.

return errors
Returns:
list[BaseSubmitter]: a list of all the submitters that were found during the
module's search
"""
return loadClasses(folder, packageName, BaseSubmitter)

Check warning on line 172 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L172

Added line #L172 was not covered by tests


class Version:
Expand Down Expand Up @@ -250,7 +261,8 @@
status = ''
# If there is a status, it is placed after a "-"
splitComponents = versionName.split("-", maxsplit=1)
if (len(splitComponents) > 1): # If there is no status, splitComponents is equal to [versionName]
# If there is no status, splitComponents is equal to [versionName]
if len(splitComponents) > 1:
status = splitComponents[-1]
return tuple([int(v) for v in splitComponents[0].split(".")]), status

Expand Down Expand Up @@ -279,7 +291,7 @@
return self.components[2]


def moduleVersion(moduleName, default=None):
def moduleVersion(moduleName: str, default=None):
""" Return the version of a module indicated with '__version__' keyword.

Args:
Expand All @@ -292,7 +304,7 @@
return getattr(sys.modules[moduleName], "__version__", default)


def nodeVersion(nodeDesc, default=None):
def nodeVersion(nodeDesc: desc.Node, default=None):
""" Return node type version for the given node description class.

Args:
Expand All @@ -305,38 +317,28 @@
return moduleVersion(nodeDesc.__module__, default)


def registerNodeType(nodeType):
""" Register a Node Type based on a Node Description class.

After registration, nodes of this type can be instantiated in a Graph.
"""
if nodeType.__name__ in nodesDesc:
logging.error(f"Node Desc {nodeType.__name__} is already registered.")
nodesDesc[nodeType.__name__] = nodeType


def unregisterNodeType(nodeType):
""" Remove 'nodeType' from the list of register node types. """
assert nodeType.__name__ in nodesDesc
del nodesDesc[nodeType.__name__]


def loadNodes(folder, packageName):
def loadNodes(folder, packageName) -> list[NodePlugin]:
if not os.path.isdir(folder):
logging.error(f"Node folder '{folder}' does not exist.")
return
return []

Check warning on line 323 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L323

Added line #L323 was not covered by tests

return loadClasses(folder, packageName, desc.BaseNode)
nodes = loadClassesNodes(folder, packageName)
return nodes


def loadAllNodes(folder):
for importer, package, ispkg in pkgutil.walk_packages([folder]):
def loadAllNodes(folder) -> list[Plugin]:
plugins = []
for _, package, ispkg in pkgutil.iter_modules([folder]):
if ispkg:
nodeTypes = loadNodes(folder, package)
for nodeType in nodeTypes:
registerNodeType(nodeType)
nodesStr = ', '.join([nodeType.__name__ for nodeType in nodeTypes])
logging.debug(f'Nodes loaded [{package}]: {nodesStr}')
plugin = Plugin(package, folder)
nodePlugins = loadNodes(folder, package)
if nodePlugins:
for node in nodePlugins:
plugin.addNodePlugin(node)
nodesStr = ', '.join([node.nodeDescriptor.__name__ for node in nodePlugins])
logging.debug(f'Nodes loaded [{package}]: {nodesStr}')
plugins.append(plugin)
return plugins


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

binFolders = [Path(folder, 'bin')]
libFolders = [Path(folder, 'lib'), Path(folder, 'lib64')]
pythonPathFolders = [Path(folder)] + binFolders
processEnv = ProcessEnv(folder)

Check warning on line 354 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L354

Added line #L354 was not covered by tests

plugins = loadAllNodes(folder=mrFolder)
if plugins:
for plugin in plugins:
pluginManager.addPlugin(plugin)
pipelineTemplates.update(plugin.templates)

Check warning on line 360 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L356-L360

Added lines #L356 - L360 were not covered by tests

loadAllNodes(folder=mrFolder)
loadPipelineTemplates(folder=mrFolder)
return plugins

Check warning on line 362 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L362

Added line #L362 was not covered by tests


def loadPluginsFolder(folder):
if not os.path.isdir(folder):
logging.debug(f"PluginSet folder '{folder}' does not exist.")
return

for file in os.listdir(folder):
if os.path.isdir(file):
subFolder = os.path.join(folder, file)
loadPluginFolder(subFolder)


def registerSubmitter(s):
def registerSubmitter(s: BaseSubmitter):
if s.name in submitters:
logging.error(f"Submitter {s.name} is already registered.")
submitters[s.name] = s
Expand All @@ -379,30 +384,31 @@
logging.error(f"Submitters folder '{folder}' does not exist.")
return

return loadClasses(folder, packageName, BaseSubmitter)

return loadClassesSubmitters(folder, packageName)

Check warning on line 387 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L387

Added line #L387 was not covered by tests

def loadPipelineTemplates(folder):
def loadPipelineTemplates(folder: str):
if not os.path.isdir(folder):
logging.error(f"Pipeline templates folder '{folder}' does not exist.")
return
for file in os.listdir(folder):
if file.endswith(".mg") and file not in pipelineTemplates:
pipelineTemplates[os.path.splitext(file)[0]] = os.path.join(folder, file)


def initNodes():
additionalNodesPath = EnvVar.getList(EnvVar.MESHROOM_NODES_PATH)
nodesFolders = [os.path.join(meshroomFolder, 'nodes')] + additionalNodesPath
nodesFolders = [os.path.join(meshroomFolder, "nodes")] + additionalNodesPath
for f in nodesFolders:
loadAllNodes(folder=f)
plugins = loadAllNodes(folder=f)
if plugins:
for plugin in plugins:
pluginManager.addPlugin(plugin)


def initSubmitters():
additionalPaths = EnvVar.getList(EnvVar.MESHROOM_SUBMITTERS_PATH)
allSubmittersFolders = [meshroomFolder] + additionalPaths
for folder in allSubmittersFolders:
subs = loadSubmitters(folder, 'submitters')
subs = loadSubmitters(folder, "submitters")

Check warning on line 411 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L411

Added line #L411 was not covered by tests
for sub in subs:
registerSubmitter(sub())

Expand All @@ -411,13 +417,15 @@
# Load pipeline templates: check in the default folder and any folder the user might have
# added to the environment variable
additionalPipelinesPath = EnvVar.getList(EnvVar.MESHROOM_PIPELINE_TEMPLATES_PATH)
pipelineTemplatesFolders = [os.path.join(meshroomFolder, 'pipelines')] + additionalPipelinesPath
pipelineTemplatesFolders = [os.path.join(meshroomFolder, "pipelines")] + additionalPipelinesPath
for f in pipelineTemplatesFolders:
loadPipelineTemplates(f)
for plugin in pluginManager.getPlugins().values():
pipelineTemplates.update(plugin.templates)


def initPlugins():
additionalpluginsPath = EnvVar.getList(EnvVar.MESHROOM_PLUGINS_PATH)
nodesFolders = [os.path.join(meshroomFolder, 'plugins')] + additionalpluginsPath
nodesFolders = [os.path.join(meshroomFolder, "plugins")] + additionalpluginsPath

Check warning on line 429 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L429

Added line #L429 was not covered by tests
for f in nodesFolders:
loadPluginFolder(folder=f)
Loading