diff --git a/src/decman/__init__.py b/src/decman/__init__.py index eb3ae5b..d4c75a8 100644 --- a/src/decman/__init__.py +++ b/src/decman/__init__.py @@ -38,3 +38,75 @@ class UserPackage: self.make_dependencies = make_dependencies self.check_dependencies = check_dependencies self.git_url = git_url + + +class Module: + """ + Collection of connected packages, services and files. + + Inherit this class to create your own modules. + """ + + def __init__(self, name: str, enabled: bool, version: str): + self.name = name + self.enabled = enabled + self.version = version + + def on_enable(self): + """ + Override this method to run python code when this module gets enabled. + """ + + def on_disable(self): + """ + Override this method to run python code when this module gets disabled. + + Note! If this module is simply removed, the code will not exacute. Instead set enabled to + False. + """ + + def after_update(self): + """ + Override this method to run python code after updating the system. If this module is + disabled, this will not run. + """ + + def after_version_change(self): + """ + Override this method to run python code after the version of this module has changed. + """ + + def pacman_packages(self) -> list[str]: + """ + Override this method to return pacman packages that should be installed as a part of this + Module. + """ + return [] + + def user_packages(self) -> list[UserPackage]: + """ + Override this method to return user packages that should be installed as a part of this + Module. + """ + return [] + + def aur_packages(self) -> list[str]: + """ + Override this method to return AUR packages that should be installed as a part of this + Module. + """ + return [] + + def systemd_units(self) -> list[str]: + """ + Override this method to return systemd units that should be enabled as a part of this + Module. + """ + return [] + + def systemd_user_units(self) -> dict[str, list[str]]: + """ + Override this method to return systemd user units that should be enabled as a part of this + Module. + """ + return {} diff --git a/src/decman/lib/__init__.py b/src/decman/lib/__init__.py index 71a553e..d5c90c3 100644 --- a/src/decman/lib/__init__.py +++ b/src/decman/lib/__init__.py @@ -132,7 +132,8 @@ class Store: def __init__(self): self.enabled_systemd_units: list[str] = [] - self.enabled_user_systemd_units: list[str] = [] + self.enabled_user_systemd_units: list[tuple[str, str]] = [] + self.enabled_modules: dict[str, str] = {} self.pkgbuild_latest_reviewed_commits: dict[str, str] = {} self._package_file_cache: dict[str, tuple[str, str]] = {} @@ -168,6 +169,7 @@ class Store: d = { "enabled_systemd_units": self.enabled_systemd_units, "enabled_user_systemd_units": self.enabled_user_systemd_units, + "enabled_modules": self.enabled_modules, "package_file_cache": self._package_file_cache, "pkgbuild_git_commits": self.pkgbuild_latest_reviewed_commits } @@ -197,13 +199,20 @@ class Store: with open(path, "rt", encoding="utf-8") as file: d = json.load(file) - store.enabled_systemd_units = d.get("enabled_systemd_units", - []) + store.enabled_systemd_units = d.get( + "enabled_systemd_units", + [], + ) store.enabled_user_systemd_units = d.get( - "enabled_user_systemd_units", []) + "enabled_user_systemd_units", + [], + ) + store.enabled_modules = d.get("enabled_modules", {}) store._package_file_cache = d.get("package_file_cache", {}) store.pkgbuild_latest_reviewed_commits = d.get( - "pkgbuild_git_commits", {}) + "pkgbuild_git_commits", + {}, + ) return store except json.JSONDecodeError as e: @@ -226,23 +235,65 @@ class Source: Configuration that describes a system. """ - def __init__(self, pacman_packages: list[str], aur_packages: list[str], - user_packages: list[decman.UserPackage], - ignored_packages: list[str], systemd_units: list[str], - systemd_user_units: dict[str, list[str]]): + def __init__( + self, + pacman_packages: list[str], + aur_packages: list[str], + user_packages: list[decman.UserPackage], + ignored_packages: list[str], + systemd_units: list[str], + systemd_user_units: dict[str, list[str]], + modules: list[decman.Module], + ): self.pacman_packages = pacman_packages self.aur_packages = aur_packages self.user_packages = user_packages self.ignored_packages = ignored_packages self.systemd_units = systemd_units self.systemd_user_units = systemd_user_units + self.modules = modules + + def run_on_enable(self, store: Store): + """ + Runs on_enable of every module that was now enabled. + """ + for module in self.modules: + if module.enabled and module.name not in store.enabled_modules: + module.on_enable() + + def run_on_disable(self, store: Store): + """ + Runs on_disable of every module that was now disabled. + """ + for module in self.modules: + if not module.enabled and module.name in store.enabled_modules: + module.on_disable() + + def run_after_update(self): + """ + Runs after_update of every enabled module. + """ + for module in self.modules: + if module.enabled: + module.after_update() + + def run_after_version_change(self, store: Store): + """ + Runs after_version_change of every enabled module that has it's version changed. + """ + for module in self.modules: + if module.enabled and module.version != store.enabled_modules.get( + module.name, module.version): + module.after_version_change() + elif module.enabled and module.name not in store.enabled_modules: + module.after_version_change() def units_to_enable(self, store: Store) -> list[str]: """ Returns all systemd units that should be enabled. """ result = [] - for unit in self.systemd_units: + for unit in self._all_units(): if unit not in store.enabled_systemd_units: result.append(unit) return result @@ -253,7 +304,7 @@ class Source: """ result = [] for unit in store.enabled_systemd_units: - if unit not in self.systemd_units: + if unit not in self._all_units(): result.append(unit) return result @@ -262,9 +313,9 @@ class Source: Returns all user systemd units that should be enabled. """ result = {} - for user, units in self.systemd_user_units.items(): + for user, units in self._all_user_units().items(): for unit in units: - stored = f"{user}: {unit}" + stored = (user, unit) if stored not in store.enabled_user_systemd_units: entry = result.get(user, []) entry.append(unit) @@ -277,11 +328,9 @@ class Source: """ result = {} for stored in store.enabled_user_systemd_units: - s = stored.split(": ") - user = s[0] - unit = s[1] + user, unit = stored - if unit not in self.systemd_user_units.get(user, []): + if unit not in self._all_user_units().get(user, []): entry = result.get(user, []) entry.append(unit) result[user] = entry @@ -293,10 +342,10 @@ class Source: Returns all packages that should be removed. This includes pacman, aur and user packages. """ result = [] - all_pkgs = self.pacman_packages + self.aur_packages + list( - map(lambda p: p.pkgname, self.user_packages)) for pkg in currently_installed_packages: - if pkg not in all_pkgs and pkg not in self.ignored_packages: + if pkg in self.ignored_packages: + continue + if pkg not in self._all_pkgs(): result.append(pkg) return result @@ -306,8 +355,10 @@ class Source: Returns all pacman packages that should be installed. """ result = [] - for pkg in self.pacman_packages: - if pkg not in currently_installed_packages and pkg not in self.ignored_packages: + for pkg in self._all_pacman_pkgs(): + if pkg in self.ignored_packages: + continue + if pkg not in currently_installed_packages: result.append(pkg) return result @@ -317,13 +368,72 @@ class Source: Returns all aur and user packages that should be installed. """ result = [] - for pkg in self.aur_packages: - if pkg not in currently_installed_packages and pkg not in self.ignored_packages: + for pkg in self._all_foreign_pkgs(): + if pkg in self.ignored_packages: + continue + if pkg not in currently_installed_packages: result.append(pkg) + return result - for pkg in self.user_packages: - if pkg.pkgname not in currently_installed_packages and pkg.pkgname not in self.ignored_packages: - result.append(pkg.pkgname) + def all_enabled_modules(self) -> list[tuple[str, str]]: + """ + Returns all enabled modules and their versions. + """ + result = [] + for module in self.modules: + if module.enabled: + result.append((module.name, module.version)) + return result + + def all_user_pkgs(self) -> list[decman.UserPackage]: + """ + Returns all active UserPackages. + """ + result = [] + result.extend(self.user_packages) + for module in self.modules: + if module.enabled: + result.extend(module.user_packages()) + return result + + def _all_pacman_pkgs(self) -> list[str]: + result = [] + result.extend(self.pacman_packages) + for module in self.modules: + if module.enabled: + result.extend(module.pacman_packages()) + return result + + def _all_foreign_pkgs(self) -> list[str]: + result = [] + result.extend(self.aur_packages) + result.extend(map(lambda p: p.pkgname, self.user_packages)) + for module in self.modules: + if module.enabled: + result.extend(module.aur_packages()) + result.extend(map(lambda p: p.pkgname, module.user_packages())) + return result + + def _all_pkgs(self) -> list[str]: + result = [] + result.extend(self._all_pacman_pkgs()) + result.extend(self._all_foreign_pkgs()) + return result + + def _all_units(self) -> list[str]: + result = [] + result.extend(self.systemd_units) + for module in self.modules: + if module.enabled: + result.extend(module.systemd_units()) + return result + + def _all_user_units(self) -> dict[str, list[str]]: + result = {} + result.update(self.systemd_user_units) + for module in self.modules: + if module.enabled: + result.update(module.systemd_user_units()) return result @@ -479,7 +589,7 @@ class Systemd: Enables the given units for the given user. """ for unit in units: - self.state.enabled_user_systemd_units.append(f"{user}: {unit}") + self.state.enabled_user_systemd_units.append((user, unit)) try: uid = pwd.getpwnam(user).pw_uid gid = pwd.getpwnam(user).pw_gid @@ -502,7 +612,7 @@ class Systemd: """ for unit in units: try: - self.state.enabled_user_systemd_units.remove(f"{user}: {unit}") + self.state.enabled_user_systemd_units.remove((user, unit)) except ValueError: pass try: diff --git a/tests/test_source_resolution.py b/tests/test_source_resolution.py index 41a4f06..4a971b4 100644 --- a/tests/test_source_resolution.py +++ b/tests/test_source_resolution.py @@ -2,12 +2,122 @@ import unittest from decman.lib import Source, Store -from decman import UserPackage +from decman import UserPackage, Module + + +class ExistingTestModule(Module): + + def __init__(self): + self.on_enable_executed = False + self.on_disable_executed = False + self.after_update_executed = False + self.after_version_change_executed = False + super().__init__("Existing", True, "1") + + def on_enable(self): + self.on_enable_executed = True + + def on_disable(self): + self.on_disable_executed = True + + def after_update(self): + self.after_update_executed = True + + def after_version_change(self): + self.after_version_change_executed = True + + +class ExistingChangedVersionTestModule(Module): + + def __init__(self): + self.on_enable_executed = False + self.on_disable_executed = False + self.after_update_executed = False + self.after_version_change_executed = False + super().__init__("ExistingChanged", True, "2") + + def on_enable(self): + self.on_enable_executed = True + + def on_disable(self): + self.on_disable_executed = True + + def after_update(self): + self.after_update_executed = True + + def after_version_change(self): + self.after_version_change_executed = True + + +class EnabledTestModule(Module): + + def __init__(self): + self.on_enable_executed = False + self.on_disable_executed = False + self.after_update_executed = False + self.after_version_change_executed = False + super().__init__("Enabled", True, "1") + + def on_enable(self): + self.on_enable_executed = True + + def on_disable(self): + self.on_disable_executed = True + + def after_update(self): + self.after_update_executed = True + + def after_version_change(self): + self.after_version_change_executed = True + + def pacman_packages(self) -> list[str]: + return ["M_p1", "M_p2", "M_p3"] + + def systemd_user_units(self) -> dict[str, list[str]]: + return {"muser": ["M_u1.service"]} + + +class DisabledTestModule(Module): + + def __init__(self): + self.on_enable_executed = False + self.on_disable_executed = False + self.after_update_executed = False + self.after_version_change_executed = False + super().__init__("Disabled", False, "1") + + def on_enable(self): + self.on_enable_executed = True + + def on_disable(self): + self.on_disable_executed = True + + def after_update(self): + self.after_update_executed = True + + def after_version_change(self): + self.after_version_change_executed = True + + def aur_packages(self) -> list[str]: + return ["M_A1", "M_A2", "M_A3"] + + def systemd_units(self) -> list[str]: + return ["M_1.service"] class TestSource(unittest.TestCase): def setUp(self): + self.disabled_module = DisabledTestModule() + self.enabled_module = EnabledTestModule() + self.existing_module = ExistingTestModule() + self.existing_module_changed = ExistingChangedVersionTestModule() + modules = [ + self.enabled_module, + self.disabled_module, + self.existing_module, + self.existing_module_changed, + ] source = Source( pacman_packages=["p1", "p2", "p3"], aur_packages=["A1", "A2", "A3"], @@ -28,12 +138,19 @@ class TestSource(unittest.TestCase): ignored_packages=["i1", "i2"], systemd_units=["1.service", "2.timer"], systemd_user_units={"user": ["u1.service", "u2.timer"]}, + modules=modules, ) store = Store() - store.enabled_systemd_units.extend(["1.service", "3.service"]) - store.enabled_user_systemd_units.extend( - ["user: u1.service", "user: u3.service"]) + store.enabled_systemd_units.extend( + ["1.service", "3.service", "M_1.service"]) + store.enabled_user_systemd_units.extend([("user", "u1.service"), + ("user", "u3.service")]) + store.enabled_modules = { + "Existing": "1", + "ExistingChanged": "1", + "Disabled": "1", + } currently_installed_packages = [ "p1", @@ -44,12 +161,48 @@ class TestSource(unittest.TestCase): "A4", "U1", "i1", + "M_p3", + "M_A1", + "M_A2", ] self.source = source self.store = store self.currently_installed_packages = currently_installed_packages + def test_after_update_executed(self): + self.source.run_after_update() + + self.assertTrue(self.enabled_module.after_update_executed) + self.assertTrue(self.existing_module.after_update_executed) + self.assertTrue(self.existing_module_changed.after_update_executed) + self.assertFalse(self.disabled_module.after_update_executed) + + def test_after_version_change_executed(self): + self.source.run_after_version_change(self.store) + + self.assertTrue(self.enabled_module.after_version_change_executed) + self.assertTrue( + self.existing_module_changed.after_version_change_executed) + self.assertFalse(self.existing_module.after_version_change_executed) + self.assertFalse(self.disabled_module.after_version_change_executed) + + def test_on_enable_executed(self): + self.source.run_on_enable(self.store) + + self.assertTrue(self.enabled_module.on_enable_executed) + self.assertFalse(self.disabled_module.on_enable_executed) + self.assertFalse(self.existing_module.on_enable_executed) + self.assertFalse(self.existing_module_changed.on_enable_executed) + + def test_on_disable_executed(self): + self.source.run_on_disable(self.store) + + self.assertTrue(self.disabled_module.on_disable_executed) + self.assertFalse(self.enabled_module.on_disable_executed) + self.assertFalse(self.existing_module.on_disable_executed) + self.assertFalse(self.existing_module_changed.on_disable_executed) + def test_units_to_enable(self): self.assertCountEqual( self.source.units_to_enable(self.store), @@ -59,13 +212,16 @@ class TestSource(unittest.TestCase): def test_units_to_disable(self): self.assertCountEqual( self.source.units_to_disable(self.store), - ["3.service"], + ["3.service", "M_1.service"], ) def test_user_units_to_enable(self): self.assertDictEqual( self.source.user_units_to_enable(self.store), - {"user": ["u2.timer"]}, + { + "user": ["u2.timer"], + "muser": ["M_u1.service"] + }, ) def test_user_units_to_disable(self): @@ -78,7 +234,7 @@ class TestSource(unittest.TestCase): self.assertCountEqual( self.source.pacman_packages_to_install( self.currently_installed_packages), - ["p3"], + ["p3", "M_p1", "M_p2"], ) def test_foreign_packages_to_install(self): @@ -91,5 +247,5 @@ class TestSource(unittest.TestCase): def test_packages_to_remove(self): self.assertCountEqual( self.source.packages_to_remove(self.currently_installed_packages), - ["p4", "A4"], + ["p4", "A4", "M_A1", "M_A2"], )