Skip to content

Plugin loader shadows an installed plugin when a same-named folder exists in the working directory #2415

Description

@Flix6x

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:

  1. __init__.py is re-executed as a fresh module, and sys.modules[plugin_name] is replaced.
  2. 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

  1. 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).
  2. FLEXMEASURES_PLUGINS = ["my_plugin"].
  3. Start FlexMeasures from the plugin repo's root directory (where the my_plugin/ folder sits).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions