Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 25 additions & 10 deletions flexmeasures/utils/plugin_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,39 @@ def register_plugins(app: Flask): # noqa: C901
return
app.config["LOADED_PLUGINS"] = {}
for plugin in plugins:
plugin_name = plugin.split("/")[-1]
is_explicit_path = (
os.path.isabs(plugin)
or "/" in plugin
or (os.sep in plugin and os.sep != "/")
or plugin.startswith(".")
)
plugin_name = (
os.path.basename(os.path.normpath(plugin))
if is_explicit_path
else plugin
)
app.logger.info(f"Importing plugin {plugin_name} ...")
module = None
if not os.path.exists(plugin): # assume plugin is a package
pkg_name = os.path.split(plugin)[
-1
] # rule out attempts for relative package imports
if not is_explicit_path:
pkg_name = plugin
app.logger.debug(
f"Attempting to import {pkg_name} as an installed package ..."
)
try:
module = importlib.import_module(pkg_name)
except ModuleNotFoundError:
app.logger.error(
f"Attempted to import module {pkg_name} (as it is not a valid file path), but it is not installed."
)
continue
else: # assume plugin is a file path
if os.path.exists(plugin):
app.logger.warning(
f"Plugin '{plugin}' is not installed in the environment; falling back to loading from relative path '{plugin}'."
)
is_explicit_path = True
else:
app.logger.error(
f"Attempted to import module {pkg_name} (as it is not a valid file path), but it is not installed."
)
continue

if is_explicit_path:
if not os.path.exists(os.path.join(plugin, "__init__.py")):
app.logger.error(
f"Plugin {plugin_name} is a valid file path, but does not contain an '__init__.py' file. Cannot load plugin {plugin_name}."
Expand Down
93 changes: 93 additions & 0 deletions flexmeasures/utils/tests/test_plugin_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import os
import sys
import types
import pytest
from flask import Flask, Blueprint

from flexmeasures.utils.plugin_utils import register_plugins


def test_installed_plugin_not_shadowed_by_cwd_folder(tmp_path, monkeypatch):
"""Test that an installed package is imported via import_module even if a folder with the same name exists in cwd."""
pkg_name = "test_installed_plugin_shadow"

# Create a mock installed module in sys.modules
mock_module = types.ModuleType(pkg_name)
mock_bp = Blueprint("installed_bp", pkg_name)
mock_module.installed_bp = mock_bp
mock_module.IS_INSTALLED = True
mock_module.__version__ = "1.0.0"

monkeypatch.setitem(sys.modules, pkg_name, mock_module)

# Create a same-named folder in tmp_path with a dummy __init__.py
plugin_dir = tmp_path / pkg_name
plugin_dir.mkdir()
(plugin_dir / "__init__.py").write_text("IS_INSTALLED = False\n", encoding="utf-8")

# Change working directory to tmp_path where the folder exists
monkeypatch.chdir(tmp_path)

app = Flask("test_app")
app.config["FLEXMEASURES_PLUGINS"] = [pkg_name]

register_plugins(app)

# Verify that the loaded plugin is the installed module and its Blueprint was registered
assert pkg_name in app.config["LOADED_PLUGINS"]
assert "installed_bp" in app.blueprints


def test_explicit_file_path_plugin(tmp_path):
"""Test that an explicit file path loads via spec_from_file_location."""
plugin_dir = tmp_path / "my_custom_filepath_plugin"
plugin_dir.mkdir()
init_file = plugin_dir / "__init__.py"
init_file.write_text(
"from flask import Blueprint\n"
"custom_bp = Blueprint('custom_bp', __name__)\n"
"__version__ = '2.0.0'\n",
encoding="utf-8",
)

app = Flask("test_app")
app.config["FLEXMEASURES_PLUGINS"] = [str(plugin_dir)]

register_plugins(app)

assert "my_custom_filepath_plugin" in app.config["LOADED_PLUGINS"]
assert "custom_bp" in app.blueprints


def test_uninstalled_plugin_falls_back_to_relative_path(tmp_path, monkeypatch):
"""Test that when a bare package name is not installed, it falls back to a relative folder in cwd with a warning."""
pkg_name = "uninstalled_fallback_plugin"
plugin_dir = tmp_path / pkg_name
plugin_dir.mkdir()
init_file = plugin_dir / "__init__.py"
init_file.write_text(
"from flask import Blueprint\n"
"fallback_bp = Blueprint('fallback_bp', __name__)\n",
encoding="utf-8",
)

monkeypatch.chdir(tmp_path)
monkeypatch.delitem(sys.modules, pkg_name, raising=False)

app = Flask("test_app")
app.config["FLEXMEASURES_PLUGINS"] = [pkg_name]

register_plugins(app)

assert pkg_name in app.config["LOADED_PLUGINS"]
assert "fallback_bp" in app.blueprints


def test_nonexistent_plugin_logs_error():
"""Test that a plugin that neither exists as a package nor as a file path logs an error."""
app = Flask("test_app")
app.config["FLEXMEASURES_PLUGINS"] = ["nonexistent_package_xyz_123"]

register_plugins(app)

assert "nonexistent_package_xyz_123" not in app.config["LOADED_PLUGINS"]
Loading