Skip to content

[core/plugins] Add PluginLoader, SubmitterProvider, and config.json name/version resolution#3171

Open
gregoire-dl wants to merge 36 commits into
developfrom
dev/enhancePluginManager
Open

[core/plugins] Add PluginLoader, SubmitterProvider, and config.json name/version resolution#3171
gregoire-dl wants to merge 36 commits into
developfrom
dev/enhancePluginManager

Conversation

@gregoire-dl

@gregoire-dl gregoire-dl commented Jul 15, 2026

Copy link
Copy Markdown
Member

Description

Introduce a new PluginLoader and a redesigned PluginManager. Change previous in code definition of a plugin (e.g. a package / subfolder of a meshroom folder). A plugin is now a folder with a meshroom subfolder possibly containing multiple standalone node description modules, submitter modules, templates and a configuration file.

Plugin:

Store for each plugin the name, version, type (BUILTIN / PATH / REZ), templates, configuration and also the:

  • NodeDescProvider objects (node description class with validity status and loading errors)
  • SubmitterProvider objects (submitter instance with validity status and loading errors)

PluginLoader:

Load/Unload plugin python modules / packages into a common virtual package following this pattern:

_meshroomPlugins.<PluginName>.<PluginSubFolder>.<Module>
_meshroomPlugins.<PluginName>.<PluginSubPkg>.<Module>
_meshroomPlugins.<PluginName>.<Module>

PluginName is defined from the REZ package name, the config.json file or in the last case the folder name of the plugin.
This pattern helps to prevent collision in plugin modules and replaces the previous Plugin.uid. The Loader ensures that only one plugin loaded with PluginName can be used (the first). All issues in the loading process are logged in a summary for each plugin loaded. Also, the loading mechanism has been updated to add support for new plugin layouts with standalone node description files and also submitter files inside the plugin.

Plugins with standalone node files are now supported (no __init__.py):

├── customPlugin/ 
│   ├── meshroom/
│   │   ├── customNodes1/
│   │   │   ├── NodeA.py
│   │   │   ├── NodeB.py
│   └── ...

Plugins without subfolders are now supported:

├── customPlugin/ 
│   ├── meshroom/
│   │   ├── NodeA.py
│   │   ├── NodeB.py
│   └── ...

Plugins with node descriptions and/or submitters:

├── customPlugin/ 
│   ├── meshroom/
│   │   ├── customNodes/
│   │   │   ├── NodeA.py
│   │   │   ├── NodeB.py
│   │   ├── customSubmitter/
│   │   │   ├── SubmitterA.py
│   └── ...

PluginManager:

Manage plugins and register node description classes and now submitter instances.
Following the previous implementation, this object centralizes all loaded plugins, available node descriptions and submitters.
The PluginManager now provides simple methods for online plugin loading:

  • addPluginFromRez for a plugin provided with REZ
  • addPluginFromPath for a plugin located at an arbitrary path
  • addPluginFromBuiltInFolder for a plugin folder using Meshroom environment (e.g. meshroom/nodes)

Unit tests:

All unit tests have been updated to follow these changes.
Multiple have been added for the new PluginLoader.

Backward Compatibility:

Fully backward-compatible, previous plugin structures and configurations remain supported.

Implementation remarks

config.json can now be written in two formats:

  • A plain list of environment variable entries:
    [
        { "key": "MY_VAR", "type": "string", "value": "myValue" },
        { "key": "MY_PATH", "type": "path", "value": "relativeOrAbsolutePath" }
    ]
  • An object with optional name, version, and env keys, env using the same entry format as above:
    {
        "name": "customPlugin",
        "version": "1.0.0",
        "env": [
            { "key": "MY_VAR", "type": "string", "value": "myValue" }
        ]
    }

Break the monolithic `core/plugins.py` module into focused submodules
under `core/plugins`:

- `env.py`: ProcessEnvType, ProcessEnv, DirTreeProcessEnv,
  RezProcessEnv, and processEnvFactory (process-environment
  resolution for plugin execution)
- `base.py`: Plugin, NodePlugin, NodePluginStatus,
  validateNodeDesc, formatNodeDescriptionErrorMessage
- `manager.py`: NodePluginManager

Update all call sites to import from the new submodules.
Change terminology for methods, function and variables. 
Update all call sites and documentation.
…nLoader`

Delete the `loadClasses` based discovery pipeline from `meshroom/core/__init__.py`
(`add_to_path`, `loadClasses(Nodes|Submitters)`, `loadAllNodes`, `loadPluginFolder`,
`registerSubmitter`, `loadAllSubmitters`, `initRezPlugins`), and rewrite
`initNodes`/`initSubmitters`/`initPlugins` to go through
`PluginManager.addPluginFromBuiltInFolder`/`addPluginFromPath`/`addPluginFromRez` instead.

`PluginManager` now owns a `PluginLoader` and registers the providers a `Plugin`
contains via `registerPluginProviders`, replacing the old `addPlugin`/`registerNode`/
`unregisterNode`. An invalid or name-colliding node/submitter provider is skipped
without failing the whole plugin load.

`Plugin` now carries its own `PluginType` and builds its own `ProcessEnv` (dirtree or
rez) at construction. `NodeDescProvider`/`SubmitterProvider` gain `absolutePackage`/
`relativePackage` so a node's process environment (and Rez subrequires) can be
resolved per sub-package within a plugin, not just per plugin.

Port every test off the removed functions onto the new `PluginManager`/`PluginLoader`
API, and flatten the test plugin fixtures from `tests/plugins/meshroom/<name>/` to
`tests/plugins/<name>/meshroom/`, matching the new one-folder-per-plugin model.
This allows to have a proper name for each built-in plugin, the name of the subfolder of the given node folder.
@gregoire-dl gregoire-dl added this to the Meshroom 2026.1.0 milestone Jul 15, 2026
@gregoire-dl gregoire-dl self-assigned this Jul 15, 2026
bool: True if the node descriptor provider has successfully been reloaded (i.e. there was
no error, and some changes were made since its last loading), False otherwise.
"""
timestamp = 0.0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR should not modify this method code logic.

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.88525% with 152 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.54%. Comparing base (1d89d80) to head (cf1bd6c).
⚠️ Report is 2 commits behind head on develop.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
meshroom/core/plugins/env.py 55.75% 50 Missing ⚠️
meshroom/core/plugins/base.py 87.02% 34 Missing ⚠️
meshroom/core/__init__.py 30.00% 21 Missing ⚠️
meshroom/core/plugins/config.py 74.64% 18 Missing ⚠️
meshroom/core/plugins/loader.py 90.32% 15 Missing ⚠️
meshroom/core/plugins/manager.py 85.26% 14 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3171      +/-   ##
===========================================
+ Coverage    86.07%   86.54%   +0.46%     
===========================================
  Files           78       83       +5     
  Lines        12111    12400     +289     
===========================================
+ Hits         10425    10731     +306     
+ Misses        1686     1669      -17     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors Meshroom's plugin loading and management system, replacing the old NodePlugin architecture with a more modular design centered around PluginManager, PluginLoader, NodeDescProvider, and SubmitterProvider. While this refactoring significantly improves plugin isolation and environment handling (including Rez and DirTree environments), several critical issues must be addressed. First, numerous invalid dictionary type hints use colons instead of commas (e.g., dict[str: str]), which will cause runtime errors. Second, splitting Rez package names with partition('-') will fail for package names containing hyphens; rpartition('-') should be used instead. Third, potential AttributeError and TypeError exceptions exist when reloading missing modules or accessing unassigned providers. Finally, file operations should explicitly specify utf-8 encoding to prevent Windows-specific decode errors, and environment path constructions must be guarded against trailing separators to avoid importing from the current working directory.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread meshroom/core/plugins/env.py Outdated
Comment thread meshroom/core/plugins/manager.py Outdated
Comment thread meshroom/core/plugins/manager.py Outdated
Comment thread meshroom/core/plugins/manager.py Outdated
Comment thread meshroom/core/plugins/base.py Outdated
Comment on lines +127 to +128
self._env["PYTHONPATH"] = os.pathsep.join(
[f"{_MESHROOM_ROOT}"] + self.pythonPaths + [os.getenv('PYTHONPATH', '')])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If os.getenv('PYTHONPATH') is empty or not set, appending "" to the path list and joining with os.pathsep will result in a trailing path separator. In Python/Linux, a trailing separator represents the current working directory (.), which can lead to unexpected module imports or security risks. It is safer to filter out empty paths.

Suggested change
self._env["PYTHONPATH"] = os.pathsep.join(
[f"{_MESHROOM_ROOT}"] + self.pythonPaths + [os.getenv('PYTHONPATH', '')])
pythonPaths = [f"{_MESHROOM_ROOT}"] + self.pythonPaths
envPythonPath = os.getenv('PYTHONPATH')
if envPythonPath:
pythonPaths.append(envPythonPath)
self._env["PYTHONPATH"] = os.pathsep.join(pythonPaths)

return False

try:
updated = importlib.reload(sys.modules.get(self.nodeDescClass.__module__))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

sys.modules.get(...) can return None if the module is not currently loaded or registered in sys.modules. Passing None to importlib.reload() will raise a TypeError. It is safer to guard against None before reloading.

Suggested change
updated = importlib.reload(sys.modules.get(self.nodeDescClass.__module__))
module = sys.modules.get(self.nodeDescClass.__module__)
if module is None:
self.status = NodeDescProviderStatus.ERROR
logging.error(f"[Reload] {self.name}: The module {self.nodeDescClass.__module__} was not found in sys.modules.")
return False
try:
updated = importlib.reload(module)

def processChunk(self, chunk):
cmd = self.buildCommandLine(chunk)
runtimeEnv = chunk.node.nodeDesc.plugin.runtimeEnv
runtimeEnv = chunk.node.nodeDesc.provider.runtimeEnv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If provider is None (e.g., if the node descriptor is instantiated directly without being registered via a NodeDescProvider), accessing provider.runtimeEnv will raise an AttributeError. Adding a defensive check prevents potential crashes.

Suggested change
runtimeEnv = chunk.node.nodeDesc.provider.runtimeEnv
runtimeEnv = chunk.node.nodeDesc.provider.runtimeEnv if chunk.node.nodeDesc.provider else None

runtimeEnv = chunk.node.nodeDesc.plugin.runtimeEnv
cmdPrefix = chunk.node.nodeDesc.plugin.commandPrefix
cmdSuffix = chunk.node.nodeDesc.plugin.commandSuffix
runtimeEnv = chunk.node.nodeDesc.provider.runtimeEnv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If provider is None (e.g., if the node descriptor is instantiated directly without being registered via a NodeDescProvider), accessing provider.runtimeEnv will raise an AttributeError. Adding a defensive check prevents potential crashes.

Suggested change
runtimeEnv = chunk.node.nodeDesc.provider.runtimeEnv
runtimeEnv = chunk.node.nodeDesc.provider.runtimeEnv if chunk.node.nodeDesc.provider else None

Comment thread tests/utils.py Outdated
@gregoire-dl
gregoire-dl marked this pull request as ready for review July 16, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant