diff --git a/README.md b/README.md
index ea362788..c8fc806b 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,8 @@ It can also be used locally for detecting problems in your addons.
- Check if all PO files are valid
+- Check for circular dependencies
+
All of the validation and checks are done according to the kodi [addon rules](https://kodi.wiki/view/Add-on_rules)
## Installation
diff --git a/kodi_addon_checker/addons/Repository.py b/kodi_addon_checker/addons/Repository.py
index 766d2e9d..e1c20b4c 100644
--- a/kodi_addon_checker/addons/Repository.py
+++ b/kodi_addon_checker/addons/Repository.py
@@ -15,13 +15,19 @@
class Repository(object):
- def __init__(self, version, path):
+ def __init__(self, version, path, local_xml=False):
super(Repository, self).__init__()
self.version = version
self.path = path
- gz_file = requests.get(path, timeout=(10, 10)).content
- with gzip.open(BytesIO(gz_file), 'rb') as xml_file:
- content = xml_file.read()
+
+ if local_xml:
+ with open(path, 'rb') as xml_file:
+ content = xml_file.read()
+ else:
+ gz_file = requests.get(path, timeout=(10, 10)).content
+ with gzip.open(BytesIO(gz_file), 'rb') as xml_file:
+ content = xml_file.read()
+
tree = ET.fromstring(content)
self.addons = []
for addon in tree.findall("addon"):
diff --git a/kodi_addon_checker/check_addon.py b/kodi_addon_checker/check_addon.py
index a3b2121c..ab18ec27 100644
--- a/kodi_addon_checker/check_addon.py
+++ b/kodi_addon_checker/check_addon.py
@@ -58,6 +58,8 @@ def start(addon_path, args, all_repo_addons, config=None):
check_dependencies.check_reverse_dependencies(addon_report, addon_id, args.branch, all_repo_addons)
+ check_dependencies.check_circular_dependencies(addon_report, all_repo_addons, parsed_xml, args.branch)
+
check_files.check_file_permission(addon_report, file_index)
check_files.check_for_invalid_xml_files(addon_report, file_index)
diff --git a/kodi_addon_checker/check_dependencies.py b/kodi_addon_checker/check_dependencies.py
index 50ba340b..7a88ea70 100644
--- a/kodi_addon_checker/check_dependencies.py
+++ b/kodi_addon_checker/check_dependencies.py
@@ -132,6 +132,58 @@ def check_reverse_dependencies(report: Report, addon: str, branch_name: str, all
.format(", ".join(sorted([r.id for r in rdependsLowerBranch])), len(rdependsLowerBranch))))
+def check_circular_dependencies(report: Report, all_repo_addons: dict, parsed_xml, branch_name: str):
+ """Check for any circular dependencies in addon.xml file and reports them
+ :all_repo_addons: dictionary return by all_repo_addon() function
+ :parsed_xml: parsed addon.xml file
+ :branch_name: name of the kodi branch/version
+ """
+ root_addon = Addon(parsed_xml)
+ ignore_list = _get_ignore_list(branch_name)
+
+ dependencies = []
+ dependency_tree = {}
+
+ # add dependencies of `addon` to dependencies list
+ def add_dependencies(addon):
+ new_depends = []
+ for dependency in addon.dependencies:
+ # don't check ignored for dependencies
+ if dependency.id in ignore_list:
+ continue
+ new_depends.append(dependency)
+
+ if addon.id not in dependency_tree:
+ dependency_tree[addon.id] = []
+ if new_depends:
+ dependency_tree[addon.id] = [d.id for d in new_depends]
+ dependencies.extend(new_depends)
+
+ add_dependencies(root_addon)
+
+ i = 0
+ # add all dependencies and their dependencies
+ while i < len(dependencies):
+ if dependencies[i].id not in dependency_tree:
+ for branch, repo in sorted(all_repo_addons.items()):
+ found_addon = repo.find(dependencies[i].id)
+ if found_addon:
+ add_dependencies(found_addon)
+ i += 1
+
+ if dependency_tree:
+ circular_dependencies = []
+ dependencies = list(dependency_tree.keys())
+
+ for depend in dependencies:
+ if root_addon.id in dependency_tree.get(depend, []):
+ circular_dependencies.append(depend)
+
+ if circular_dependencies:
+ report.add(Record(PROBLEM, "Circular dependencies: {}"
+ .format(", ".join(sorted(set(circular_dependencies))))))
+
+
def _get_ignore_list(branch_name):
if branch_name == "leia":
diff --git a/tests/test_check_dependencies.py b/tests/test_check_dependencies.py
new file mode 100644
index 00000000..45a78a00
--- /dev/null
+++ b/tests/test_check_dependencies.py
@@ -0,0 +1,39 @@
+import unittest
+
+from os.path import abspath, dirname, join
+import xml.etree.ElementTree as ET
+
+from kodi_addon_checker.addons.Repository import Repository
+from kodi_addon_checker.check_dependencies import check_circular_dependencies
+from kodi_addon_checker.common import load_plugins
+from kodi_addon_checker.record import Record
+from kodi_addon_checker.reporter import ReportManager
+from kodi_addon_checker.report import Report
+
+
+HERE = abspath(dirname(__file__))
+
+
+class TestCheckDependencies(unittest.TestCase):
+
+ def setUp(self):
+ load_plugins()
+ ReportManager.enable(["array"])
+ self.report = Report("")
+ self.branch = 'krypton'
+
+ def test_check_circular_dependency(self):
+ self.path = join(HERE, 'test_data', 'Circular_depend')
+ addon_xml = join(self.path, "addon.xml")
+ addons_xml = join(self.path, "addons.xml")
+
+ parsed_xml = ET.parse(addon_xml).getroot()
+ all_repo_addons = {self.branch: Repository(self.branch, addons_xml, local_xml=True)}
+
+ check_circular_dependencies(self.report, all_repo_addons, parsed_xml, self.branch)
+
+ records = [Record.__str__(r) for r in ReportManager.getEnabledReporters()[0].reports]
+ self.output = [record for record in records if record.startswith("ERROR: Circular")]
+ self.expected = ["ERROR: Circular dependencies: plugin.test.one"]
+
+ self.assertListEqual(self.expected, self.output)
diff --git a/tests/test_data/Circular_depend/addon.xml b/tests/test_data/Circular_depend/addon.xml
new file mode 100644
index 00000000..921380cb
--- /dev/null
+++ b/tests/test_data/Circular_depend/addon.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ video
+
+
+ Testing
+ Testing
+ en
+ This is just a test
+ all
+ GPL-3.0-only
+
+ icon.png
+ fanart.jpg
+
+ Testing 123
+
+
diff --git a/tests/test_data/Circular_depend/addons.xml b/tests/test_data/Circular_depend/addons.xml
new file mode 100644
index 00000000..bfb3ce0a
--- /dev/null
+++ b/tests/test_data/Circular_depend/addons.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+ video
+
+
+ Testing
+ Testing
+ en
+ This is just a test
+ all
+ GPL-3.0-only
+
+ icon.png
+ fanart.jpg
+
+ Testing 123
+
+
+