What happens
register_plugins (flexmeasures/utils/plugin_utils.py) decides whether a FLEXMEASURES_PLUGINS entry is a file path or an installed package like this:
if not os.path.exists(plugin): # assume plugin is a package
...importlib.import_module(pkg_name)
else: # assume plugin is a file path
...
module = importlib.util.module_from_spec(spec)
sys.modules[plugin_name] = module
spec.loader.exec_module(module)
os.path.exists on a bare name like my_plugin is a relative check against the current working directory. So when a plugin is properly installed (e.g. pip install -e .) and the server happens to be started from a directory that contains a folder with the plugin's name (very common: the plugin's own repo root, which contains the package folder), the loader takes the file-path branch instead of importing the installed package.
The consequences are worse than just loading the same code twice:
__init__.py is re-executed as a fresh module, and sys.modules[plugin_name] is replaced.
- Any submodules already imported (e.g.
my_plugin.views, imported by the first execution) still reference the old module object. With the common Blueprint pattern — __init__.py creates the Blueprint, views.py imports it and attaches routes — the freshly executed __init__.py creates a new, empty Blueprint, while all routes/CLI commands hang off the stale one. FlexMeasures then registers the empty Blueprint: the plugin appears loaded (shows in the footer), but its routes 404 and its CLI group has no commands.
Reproduce
pip install -e . a minimal plugin (package my_plugin, Blueprint in __init__.py, one route added in views.py via from my_plugin import bp).
FLEXMEASURES_PLUGINS = ["my_plugin"].
- Start FlexMeasures from the plugin repo's root directory (where the
my_plugin/ folder sits).
- The route 404s. Start the server from any other directory and it works.
Suggested fix
Only treat an entry as a file path when it looks like one (contains os.sep, or os.path.isabs), or check importlib.util.find_spec(pkg_name) first and fall back to the path branch. At minimum, log a loud warning when a relative name is resolved via cwd, since the resulting failure mode (empty Blueprint) is silent and confusing.
Happy to turn this into a PR if the direction is agreed.
What happens
register_plugins(flexmeasures/utils/plugin_utils.py) decides whether aFLEXMEASURES_PLUGINSentry is a file path or an installed package like this:os.path.existson a bare name likemy_pluginis a relative check against the current working directory. So when a plugin is properly installed (e.g.pip install -e .) and the server happens to be started from a directory that contains a folder with the plugin's name (very common: the plugin's own repo root, which contains the package folder), the loader takes the file-path branch instead of importing the installed package.The consequences are worse than just loading the same code twice:
__init__.pyis re-executed as a fresh module, andsys.modules[plugin_name]is replaced.my_plugin.views, imported by the first execution) still reference the old module object. With the common Blueprint pattern —__init__.pycreates the Blueprint,views.pyimports it and attaches routes — the freshly executed__init__.pycreates a new, empty Blueprint, while all routes/CLI commands hang off the stale one. FlexMeasures then registers the empty Blueprint: the plugin appears loaded (shows in the footer), but its routes 404 and its CLI group has no commands.Reproduce
pip install -e .a minimal plugin (packagemy_plugin, Blueprint in__init__.py, one route added inviews.pyviafrom my_plugin import bp).FLEXMEASURES_PLUGINS = ["my_plugin"].my_plugin/folder sits).Suggested fix
Only treat an entry as a file path when it looks like one (contains
os.sep, oros.path.isabs), or checkimportlib.util.find_spec(pkg_name)first and fall back to the path branch. At minimum, log a loud warning when a relative name is resolved via cwd, since the resulting failure mode (empty Blueprint) is silent and confusing.Happy to turn this into a PR if the direction is agreed.