Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fixes:
- fix: removed deprecated argument reconnection_interval for irc v20.0 (#1568)
- docs: Add Gentoo packages (#1567)
- chore: bump actions/setup-python from 3.1.0 to 3.1.2 (#1564)
- fix: circular dependencies error when there are none (#1505)

v6.1.8 (2021-06-21)
-------------------
Expand Down
37 changes: 34 additions & 3 deletions errbot/plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import sys
import traceback
from copy import deepcopy
from importlib import machinery
from graphlib import CycleError
from graphlib import TopologicalSorter as BaseTopologicalSorter
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type

Expand Down Expand Up @@ -165,6 +166,12 @@ def check_errbot_version(plugin_info: PluginInfo):
BL_PLUGINS = "bl_plugins"


class TopologicalSorter(BaseTopologicalSorter):
def find_cycle(self):
"""Wraps private method as public one."""
return self._find_cycle()


class BotPluginManager(StoreMixin):
def __init__(
self,
Expand Down Expand Up @@ -421,11 +428,12 @@ def activate_non_started_plugins(self) -> None:
"""
Activates all plugins that are not activated, respecting its dependencies.

:return: Empty string if no problem occured or a string explaining what went wrong.
:return: Empty string if no problem occurred or a string explaining what went wrong.
"""
log.info("Activate bot plugins...")
errors = ""
for name, plugin in self.plugins.items():
for name in self.get_plugins_activation_order():
plugin = self.plugins.get(name)
try:
if self.is_plugin_blacklisted(name):
errors += (
Expand All @@ -451,6 +459,29 @@ def activate_non_started_plugins(self) -> None:
errors += f"Error: flow {name} failed to start: {e}.\n"
return errors

def get_plugins_activation_order(self) -> List[str]:
"""
Calculate plugin activation order, based on their dependencies.

:return: list of plugin names, in the best order to start them.
"""
plugins_graph = {
name: set(info.dependencies) for name, info in self.plugin_infos.items()
}
plugins_in_cycle = set()
while True:
plugins_sorter = TopologicalSorter(plugins_graph)
try:
# Return plugins which are part of a circular dependency at the end,
# the rest of the code expects to have all plugins returned
return list(plugins_sorter.static_order()) + list(plugins_in_cycle)
except CycleError:
# Remove cycle from the graph, and
cycle = set(plugins_sorter.find_cycle())
plugins_in_cycle.update(cycle)
for plugin_name in cycle:
plugins_graph.pop(plugin_name)

def _activate_plugin(self, plugin: BotPlugin, plugin_info: PluginInfo) -> None:
"""
Activate a specific plugin with no check.
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"deepmerge==1.0.1",
]

if py_version < (3, 9):
deps.append("graphlib-backport==1.0.3")

src_root = os.curdir


Expand Down
12 changes: 6 additions & 6 deletions tests/circular_dependencies_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
pytest_plugins = ["errbot.backends.test"]


# def test_if_all_loaded_circular_dependencies(testbot):
# """https://github.com/errbotio/errbot/issues/1397"""
# plug_names = testbot.bot.plugin_manager.get_all_active_plugin_names()
# assert "PluginA" in plug_names
# assert "PluginB" in plug_names
# assert "PluginC" in plug_names
def test_if_all_loaded_circular_dependencies(testbot):
"""https://github.com/errbotio/errbot/issues/1397"""
plug_names = testbot.bot.plugin_manager.get_all_active_plugin_names()
assert "PluginA" in plug_names
assert "PluginB" in plug_names
assert "PluginC" in plug_names
4 changes: 4 additions & 0 deletions tests/commands_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ def test_broken_plugin(testbot):
)
assert "import borken # fails" in testbot.pop_message()
assert "as it did not load correctly." in testbot.pop_message()
assert (
"Error: Broken failed to activate: "
"'NoneType' object has no attribute 'is_activated'"
) in testbot.pop_message()
Comment on lines +172 to +175
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert "Plugins reloaded." in testbot.pop_message()
finally:
rmtree(tempd)
Expand Down
7 changes: 7 additions & 0 deletions tests/dependencies_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,10 @@ def test_indirect_circular_dependency(testbot):
assert "Circular2" not in plug_names
assert "Circular3" not in plug_names
assert "Circular4" not in plug_names


def test_chained_dependency(testbot):
plug_names = testbot.bot.plugin_manager.get_all_active_plugin_names()
assert "Chained1" in plug_names
assert "Chained2" in plug_names
assert "Chained3" in plug_names
3 changes: 3 additions & 0 deletions tests/dependent_plugins/chained1.plug
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[Core]
Name = Chained1
Module = chained1
5 changes: 5 additions & 0 deletions tests/dependent_plugins/chained1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from errbot import BotPlugin


class Chained1(BotPlugin):
pass
4 changes: 4 additions & 0 deletions tests/dependent_plugins/chained2.plug
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Core]
Name = Chained2
Module = chained2
DependsOn = Chained1
5 changes: 5 additions & 0 deletions tests/dependent_plugins/chained2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from errbot import BotPlugin


class Chained2(BotPlugin):
pass
4 changes: 4 additions & 0 deletions tests/dependent_plugins/chained3.plug
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Core]
Name = Chained3
Module = chained3
DependsOn = Chained2, Chained1
5 changes: 5 additions & 0 deletions tests/dependent_plugins/chained3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from errbot import BotPlugin


class Chained3(BotPlugin):
pass