Skip to content

Commit 2b56971

Browse files
browniebrokesijis
andauthored
fix: circular dependencies error when there are none (#1505)
* test: reproduce bug with wrong circular dependencies Add a new test case reproducing the problem reported in #1397 * chore: fix typos in comment and docstring * fix: activate plugins in deterministic order * test: update test case with latest changes * fix: set while at True * test: re-enable circular testing * fix: pin version * docs: add info to CHANGES Co-authored-by: Sijis Aviles <[email protected]>
1 parent 76845ea commit 2b56971

File tree

12 files changed

+81
-9
lines changed

12 files changed

+81
-9
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fixes:
3434
- fix: removed deprecated argument reconnection_interval for irc v20.0 (#1568)
3535
- docs: Add Gentoo packages (#1567)
3636
- chore: bump actions/setup-python from 3.1.0 to 3.1.2 (#1564)
37+
- fix: circular dependencies error when there are none (#1505)
3738

3839
v6.1.8 (2021-06-21)
3940
-------------------

errbot/plugin_manager.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import sys
66
import traceback
77
from copy import deepcopy
8-
from importlib import machinery
8+
from graphlib import CycleError
9+
from graphlib import TopologicalSorter as BaseTopologicalSorter
910
from pathlib import Path
1011
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
1112

@@ -165,6 +166,12 @@ def check_errbot_version(plugin_info: PluginInfo):
165166
BL_PLUGINS = "bl_plugins"
166167

167168

169+
class TopologicalSorter(BaseTopologicalSorter):
170+
def find_cycle(self):
171+
"""Wraps private method as public one."""
172+
return self._find_cycle()
173+
174+
168175
class BotPluginManager(StoreMixin):
169176
def __init__(
170177
self,
@@ -421,11 +428,12 @@ def activate_non_started_plugins(self) -> None:
421428
"""
422429
Activates all plugins that are not activated, respecting its dependencies.
423430
424-
:return: Empty string if no problem occured or a string explaining what went wrong.
431+
:return: Empty string if no problem occurred or a string explaining what went wrong.
425432
"""
426433
log.info("Activate bot plugins...")
427434
errors = ""
428-
for name, plugin in self.plugins.items():
435+
for name in self.get_plugins_activation_order():
436+
plugin = self.plugins.get(name)
429437
try:
430438
if self.is_plugin_blacklisted(name):
431439
errors += (
@@ -451,6 +459,29 @@ def activate_non_started_plugins(self) -> None:
451459
errors += f"Error: flow {name} failed to start: {e}.\n"
452460
return errors
453461

462+
def get_plugins_activation_order(self) -> List[str]:
463+
"""
464+
Calculate plugin activation order, based on their dependencies.
465+
466+
:return: list of plugin names, in the best order to start them.
467+
"""
468+
plugins_graph = {
469+
name: set(info.dependencies) for name, info in self.plugin_infos.items()
470+
}
471+
plugins_in_cycle = set()
472+
while True:
473+
plugins_sorter = TopologicalSorter(plugins_graph)
474+
try:
475+
# Return plugins which are part of a circular dependency at the end,
476+
# the rest of the code expects to have all plugins returned
477+
return list(plugins_sorter.static_order()) + list(plugins_in_cycle)
478+
except CycleError:
479+
# Remove cycle from the graph, and
480+
cycle = set(plugins_sorter.find_cycle())
481+
plugins_in_cycle.update(cycle)
482+
for plugin_name in cycle:
483+
plugins_graph.pop(plugin_name)
484+
454485
def _activate_plugin(self, plugin: BotPlugin, plugin_info: PluginInfo) -> None:
455486
"""
456487
Activate a specific plugin with no check.

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
"deepmerge==1.0.1",
4343
]
4444

45+
if py_version < (3, 9):
46+
deps.append("graphlib-backport==1.0.3")
47+
4548
src_root = os.curdir
4649

4750

tests/circular_dependencies_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
pytest_plugins = ["errbot.backends.test"]
77

88

9-
# def test_if_all_loaded_circular_dependencies(testbot):
10-
# """https://github.com/errbotio/errbot/issues/1397"""
11-
# plug_names = testbot.bot.plugin_manager.get_all_active_plugin_names()
12-
# assert "PluginA" in plug_names
13-
# assert "PluginB" in plug_names
14-
# assert "PluginC" in plug_names
9+
def test_if_all_loaded_circular_dependencies(testbot):
10+
"""https://github.com/errbotio/errbot/issues/1397"""
11+
plug_names = testbot.bot.plugin_manager.get_all_active_plugin_names()
12+
assert "PluginA" in plug_names
13+
assert "PluginB" in plug_names
14+
assert "PluginC" in plug_names

tests/commands_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ def test_broken_plugin(testbot):
169169
)
170170
assert "import borken # fails" in testbot.pop_message()
171171
assert "as it did not load correctly." in testbot.pop_message()
172+
assert (
173+
"Error: Broken failed to activate: "
174+
"'NoneType' object has no attribute 'is_activated'"
175+
) in testbot.pop_message()
172176
assert "Plugins reloaded." in testbot.pop_message()
173177
finally:
174178
rmtree(tempd)

tests/dependencies_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,10 @@ def test_indirect_circular_dependency(testbot):
5858
assert "Circular2" not in plug_names
5959
assert "Circular3" not in plug_names
6060
assert "Circular4" not in plug_names
61+
62+
63+
def test_chained_dependency(testbot):
64+
plug_names = testbot.bot.plugin_manager.get_all_active_plugin_names()
65+
assert "Chained1" in plug_names
66+
assert "Chained2" in plug_names
67+
assert "Chained3" in plug_names
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[Core]
2+
Name = Chained1
3+
Module = chained1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from errbot import BotPlugin
2+
3+
4+
class Chained1(BotPlugin):
5+
pass
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[Core]
2+
Name = Chained2
3+
Module = chained2
4+
DependsOn = Chained1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from errbot import BotPlugin
2+
3+
4+
class Chained2(BotPlugin):
5+
pass

0 commit comments

Comments
 (0)