Describe the bug
_initPythonPlugin in src/plugin.py has two load paths, and only one of them registers the
module:
if location is None:
mod = importlib.import_module(name) # registers in sys.modules
else:
...
spec = importlib.util.spec_from_file_location(name, modpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # does NOT register in sys.modules
Every plugin in config/plugins.yaml carries a location, so in a default install they all take
the second path. The module object then lives only in _plugins[name]. Any other code that does
import <name> does not find it in sys.modules and imports the file a second time, getting
a separate module object with its own globals.
For a channel plugin that is not a cosmetic issue: module-level state is duplicated. A second copy
of channels/telegram.py has its own outbox and its own polling state, and nothing polls it, so
sends written into it are never delivered and nothing reports an error.
The two paths also disagree with each other — the same plugin is a shared module when found on
PYTHONPATH and a private one when found by location.
To Reproduce
- Run with a plugin that has a
location, e.g. the default telegram.
- From any other module in the process,
import telegram.
telegram is not in sys.modules beforehand, so the import executes the file again. The
object obtained is not the one the plugin loader started: state set during
loadOmegaPlugin() is absent, and id() differs from plugin._plugins["telegram"].mod.
We hit this on our own deployment: a helper module that had previously done a plain
import telegram kept working by accident until the plugin API landed, then silently began
addressing a second, never-polled copy. The symptom was outbound messages disappearing with no
error line.
Expected behavior
A plugin loaded by location is the same module object that import <name> yields, as it is on the
PYTHONPATH path.
Possible fix
The standard importlib recipe, registering before execution so the module is also visible to
imports made while it is initialising:
spec = importlib.util.spec_from_file_location(name, modpath)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
That makes the two load paths behave alike.
One thing to weigh before doing it that way: registering under the bare name puts the plugin in
the same namespace as installed packages, so a plugin called telegram would shadow a PyPI
telegram for anything importing it later. The PYTHONPATH path already has that property, so
this would make the behaviour consistent rather than introduce it — but if isolation is the point,
a namespaced key such as plugins.telegram gives a single shared object without touching the
top-level namespace.
If the isolation is deliberate as it stands, then it is worth stating in
docs/reference-plugin-api.md, since a plugin author has no way to discover it except by losing
state.
Environment
Code path above is current main.
Describe the bug
_initPythonPlugininsrc/plugin.pyhas two load paths, and only one of them registers themodule:
Every plugin in
config/plugins.yamlcarries alocation, so in a default install they all takethe second path. The module object then lives only in
_plugins[name]. Any other code that doesimport <name>does not find it insys.modulesand imports the file a second time, gettinga separate module object with its own globals.
For a channel plugin that is not a cosmetic issue: module-level state is duplicated. A second copy
of
channels/telegram.pyhas its own outbox and its own polling state, and nothing polls it, sosends written into it are never delivered and nothing reports an error.
The two paths also disagree with each other — the same plugin is a shared module when found on
PYTHONPATHand a private one when found by location.To Reproduce
location, e.g. the defaulttelegram.import telegram.telegramis not insys.modulesbeforehand, so the import executes the file again. Theobject obtained is not the one the plugin loader started: state set during
loadOmegaPlugin()is absent, andid()differs fromplugin._plugins["telegram"].mod.We hit this on our own deployment: a helper module that had previously done a plain
import telegramkept working by accident until the plugin API landed, then silently beganaddressing a second, never-polled copy. The symptom was outbound messages disappearing with no
error line.
Expected behavior
A plugin loaded by location is the same module object that
import <name>yields, as it is on thePYTHONPATHpath.Possible fix
The standard importlib recipe, registering before execution so the module is also visible to
imports made while it is initialising:
That makes the two load paths behave alike.
One thing to weigh before doing it that way: registering under the bare name puts the plugin in
the same namespace as installed packages, so a plugin called
telegramwould shadow a PyPItelegramfor anything importing it later. ThePYTHONPATHpath already has that property, sothis would make the behaviour consistent rather than introduce it — but if isolation is the point,
a namespaced key such as
plugins.telegramgives a single shared object without touching thetop-level namespace.
If the isolation is deliberate as it stands, then it is worth stating in
docs/reference-plugin-api.md, since a plugin author has no way to discover it except by losingstate.
Environment
Code path above is current
main.