Skip to content

Prefer an installed plugin package over a same-named folder in the working directory - #2419

Open
Flix6x wants to merge 4 commits into
mainfrom
fix/2415-prefer-installed-plugin-package
Open

Prefer an installed plugin package over a same-named folder in the working directory#2419
Flix6x wants to merge 4 commits into
mainfrom
fix/2415-prefer-installed-plugin-package

Conversation

@Flix6x

@Flix6x Flix6x commented Aug 8, 2026

Copy link
Copy Markdown
Member

The bug

register_plugins decided whether a FLEXMEASURES_PLUGINS entry is a file path or an installed package with os.path.exists(plugin). On a bare name like my_plugin, that is a check relative to the working directory. So an installed plugin was loaded by path whenever a folder of that name happened to sit in the cwd — which is exactly the case when you start FlexMeasures from the plugin's own repository.

Loading by path re-executes __init__.py under a new module object and replaces sys.modules[plugin_name]. With the common Blueprint layout (__init__.py creates the Blueprint, views.py imports it and attaches routes), the submodules imported by the first execution keep referring to the old module, so the Blueprint that FlexMeasures registers is a fresh, empty one. The plugin shows up as loaded, and its routes 404 and its CLI group is empty.

Closes #2415.

The fix

As @nhoening put it in the issue: "If the package is installed, that should be used."

The name is now looked up with importlib.util.find_spec first, and imported as a package when one is found. An entry that is spelled out as a path (absolute, or containing a separator, e.g. ./my_plugin) still loads exactly the folder it points to, so the documented file-path usage is unchanged.

Two details worth a look:

  • Namespace packages are ignored in the lookup. A folder without an __init__.py is importable as a namespace package, so accepting it would have replaced today's clear "does not contain an __init__.py file" error with an empty package and a vaguer "no blueprints found" warning.
  • A bare name that resolves to a cwd folder now warns, since that is the one case the loader cannot tell apart from a mistyped package name — the issue asked for this as a minimum.

Does this change our current preference?

Yes, deliberately, and only for the ambiguous case: a bare name for which both an importable package and a same-named folder exist. That used to load the folder, and now loads the package. Everything else resolves as before:

Entry Installed package Folder in cwd Before After
my_plugin folder (the bug) package
my_plugin package package
my_plugin folder folder (now with a warning)
./my_plugin or /abs/my_plugin folder folder

The test

flexmeasures/utils/tests/test_plugin_utils.py is new — the loader had no tests. Each test writes a throwaway plugin whose Blueprint gets its route from a submodule, the way real plugins are laid out, and asserts on the app's url_map. That matters: LOADED_PLUGINS looked perfectly healthy while the bug was live, and only the routing table shows that the registered Blueprint lost its routes.

Covered: the shadowing case (regression), a bare name with nothing installed, relative and absolute path entries, a folder without __init__.py, and a name that is neither installed nor present.

Mutation-tested (each mutant applied to plugin_utils.py, tests rerun):

Mutant Outcome
Old cwd-first precedence (prefer_package = False) killed
is_written_as_path always False (paths treated as names) killed
is_written_as_path always True (names treated as paths) killed
Namespace packages accepted in the lookup killed

Pristine: 6 passed; full flexmeasures/utils package: 253 passed.

How to test

Reproduce as in the issue: pip install -e . a plugin whose __init__.py defines the Blueprint and whose views.py adds a route, set FLEXMEASURES_PLUGINS = ["my_plugin"], and start FlexMeasures from the plugin repo's root. The route used to 404; it now works, and the loaded module is the installed one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FxXYu3pPcB23HaUie3aK2d

Flix6x added 4 commits August 8, 2026 15:57
…med folder

Context:
- GH issue #2415: os.path.exists on a bare FLEXMEASURES_PLUGINS entry is a check
  relative to the working directory, so starting FlexMeasures from a plugin's own
  repository made the loader take the file path branch for an installed plugin.
  That re-executes __init__.py under a new module object while submodules keep
  referring to the old one, so the Blueprint that gets registered is the empty one
  and the plugin's routes and CLI commands go missing, silently

Change:
- Look the name up with importlib.util.find_spec first, and import the package when
  one is found, unless the entry is spelled out as a path (absolute or containing a
  separator), which still loads exactly the folder it points to
- Ignore namespace packages when looking up, so a folder without an __init__.py keeps
  reporting that, rather than loading as an empty package
- Warn when a bare name resolves to a folder in the working directory, as that is the
  case the loader cannot distinguish from a typo'd package name

Signed-off-by: F.N. Claessen <felix@seita.nl>
Context:
- The loader had no tests, and the bug in #2415 was invisible in the loaded-plugins
  listing: only the routing table shows that the registered Blueprint lost its routes

Change:
- Added test_plugin_utils.py, writing throwaway plugins whose Blueprint gets its route
  from a submodule, as real plugins do, and asserting on the resulting url_map
- Covers the shadowing case, a bare name with nothing installed, relative and absolute
  path entries, a folder without __init__.py, and a name that is neither

Signed-off-by: F.N. Claessen <felix@seita.nl>
Context:
- The precedence between an installed package and a same-named folder was neither
  documented nor obvious

Change:
- Documented it under FLEXMEASURES_PLUGINS, including how to load a folder on purpose
- Added a changelog entry

Signed-off-by: F.N. Claessen <felix@seita.nl>
Context:
- The entry was written before the PR existed

Change:
- Pointed it at PR #2419

Signed-off-by: F.N. Claessen <felix@seita.nl>
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 flexmeasures | 🛠️ Build #33972935 | 📁 Comparing 11ae338 against latest (18f43b5)

  🔍 Preview build  

5 files changed · ± 5 modified

± Modified

@Flix6x
Flix6x requested a review from nhoening August 8, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant