From c9db948fb84838676cc304023be43c97a215d6ba Mon Sep 17 00:00:00 2001 From: Marco Ceppi Date: Sun, 6 Aug 2023 22:55:46 -0400 Subject: [PATCH] fix: support for multiple package screens in one config (#139) --- .github/workflows/ci.yml | 4 +- tests/example-multi-package.yml | 69 ++++++++++++++++++++++++++ tests/test_screen_package_state.py | 25 ++++------ yafti/__main__.py | 11 +++- yafti/app.py | 17 ++++--- yafti/screen/package/screen/install.py | 6 ++- yafti/screen/package/screen/package.py | 13 +++-- yafti/screen/package/screen/picker.py | 14 +++--- yafti/screen/package/state.py | 20 ++++---- yafti/screen/package/utils.py | 6 +++ 10 files changed, 135 insertions(+), 50 deletions(-) create mode 100644 tests/example-multi-package.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a9c679..f7e992c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,9 @@ jobs: virtualenvs-create: true virtualenvs-in-project: true - name: System Deps - run: sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev + run: | + sudo apt update + sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev - name: Cache Dependencies id: cache-deps uses: actions/cache@v3 diff --git a/tests/example-multi-package.yml b/tests/example-multi-package.yml new file mode 100644 index 0000000..aa31341 --- /dev/null +++ b/tests/example-multi-package.yml @@ -0,0 +1,69 @@ +title: uBlue First Boot +properties: + mode: "run-on-change" +actions: + pre: + - run: /full/path/to/bin --with --params + - run: /another/command run + - yafti.plugin.flatpak: + install: org.gnome.Calculator + post: + - run: /run/these/commands --after --all --screens +screens: + first-screen: + source: yafti.screen.title + values: + title: "That was pretty cool" + icon: "/path/to/icon" + description: | + Time to play overwatch + can-we-modify-your-flatpaks: + source: yafti.screen.consent + values: + title: Welcome traveler + condition: + run: flatpak remotes --system | grep fedora + description: | + This tool modifies your flatpaks and flatpak sources. If you do not want to do this exit the installer. + For new users just do it (tm) + actions: + - run: flatpak remote-delete fedora --force + - run: flatpak remove --system --noninteractive --all + applications: + source: yafti.screen.package + values: + title: Install flatpaks + show_terminal: true + package_manager: yafti.plugin.flatpak + groups: + Core: + description: All the good stuff + packages: + - Calculator: org.gnome.Calculator + - Firefox: org.mozilla.firefox + Gaming: + description: GAMES GAMES GAMES + default: false + packages: + - Steam: com.valvesoftware.Steam + - Games: org.gnome.Games + applications-two: + source: yafti.screen.package + values: + title: Install more flatpaks + show_terminal: true + package_manager: yafti.plugin.flatpak + groups: + Office: + description: All the work stuff + default: false + packages: + - LibreOffice: org.libreoffice.LibreOffice + - Calendar: org.gnome.Calendar + final-screen: + source: yafti.screen.title + values: + title: "All done" + icon: "/atph/to/icon" + description: | + Thanks for installing, join the community, next steps diff --git a/tests/test_screen_package_state.py b/tests/test_screen_package_state.py index 864a086..5b7edbd 100644 --- a/tests/test_screen_package_state.py +++ b/tests/test_screen_package_state.py @@ -4,34 +4,27 @@ def test_state_set(): - state = PackageScreenState() + state = PackageScreenState("test_state_set") state.set("hello", True) assert state.get("hello") is True def test_state_set_fail(): - state = PackageScreenState() + state = PackageScreenState("test_state_set_fail") with pytest.raises(ValidationError): state.set("hello", "world") def test_state_load(): input = {"hello": True, "world": False} - state = PackageScreenState() + state = PackageScreenState("test_state_load") state.load(input) assert state.get("hello") is True assert state.get("world") is False -def test_state_from_dict(): - input = {"hello": True, "world": False} - state = PackageScreenState.from_dict(input) - assert state.get("hello") is True - assert state.get("world") is False - - def test_state_remove(): - state = PackageScreenState() + state = PackageScreenState("test_state_remove") state.set("kenobi", False) state.set("general", True) assert state.get("kenobi") is False @@ -42,7 +35,7 @@ def test_state_remove(): def test_state_on_off(): - state = PackageScreenState() + state = PackageScreenState("test_state_on_off") state.on("grievous") assert state.get("grievous") is True state.off("grievous") @@ -55,7 +48,7 @@ def test_state_on_off(): def test_state_toggle(): - state = PackageScreenState() + state = PackageScreenState("test_state_toggle") state.on("chewy") assert state.get("chewy") is True state.toggle("chewy") @@ -65,13 +58,13 @@ def test_state_toggle(): def test_state_toggle_error(): - state = PackageScreenState() + state = PackageScreenState("test_state_toggle_error") with pytest.raises(KeyError): state.toggle("barf") def test_state_get_on(): - state = PackageScreenState() + state = PackageScreenState("test_state_get_on") state.on("chewy") state.on("han") state.off("greedo") @@ -81,7 +74,7 @@ def test_state_get_on(): def test_state_keys(): - state = PackageScreenState() + state = PackageScreenState("test_state_keys") state.on("AA") state.on("BB") state.off("CC") diff --git a/yafti/__main__.py b/yafti/__main__.py index d249021..e5ba677 100644 --- a/yafti/__main__.py +++ b/yafti/__main__.py @@ -15,6 +15,7 @@ """ import logging +from typing import Annotated import typer import yaml @@ -25,12 +26,18 @@ from yafti.parser import Config -def run(config: typer.FileText = typer.Argument("/etc/yafti.yml"), debug: bool = False): +def run( + config: typer.FileText = typer.Argument("/etc/yafti.yml"), + debug: bool = False, + force_run: Annotated[ + bool, typer.Option("-f", "--force", help="Ignore run mode and force run") + ] = False, +): log.set_level(logging.DEBUG if debug else logging.INFO) log.debug("starting up", config=config, debug=debug) config = Config.parse_obj(yaml.safe_load(config)) app = Yafti(config) - app.run(None) + app.run(None, force_run=force_run) def app(): diff --git a/yafti/app.py b/yafti/app.py index f7fbec7..e99c11e 100644 --- a/yafti/app.py +++ b/yafti/app.py @@ -31,7 +31,7 @@ def __init__(self, cfg: Config = None, loop=None): self.config = cfg self.loop = loop or gbulb.get_event_loop() - def run(self, *args, **kwargs): + def run(self, *args, force_run: bool = False, **kwargs): configured_mode = self.config.properties.mode _p: Path = self.config.properties.path.expanduser() # TODO(GH-#103): Remove this prior to 1.0 release. Start. @@ -43,15 +43,16 @@ def run(self, *args, **kwargs): _p.unlink() _old_p.rename(_p) # TODO(GH-#103): End. - if configured_mode == YaftiRunModes.disable: - return - - if configured_mode == YaftiRunModes.changed: - if _p.exists() and _p.read_text() == self.config_sha: + if not force_run: + if configured_mode == YaftiRunModes.disable: return - if configured_mode == YaftiRunModes.ignore and _p.exists(): - return + if configured_mode == YaftiRunModes.changed: + if _p.exists() and _p.read_text() == self.config_sha: + return + + if configured_mode == YaftiRunModes.ignore and _p.exists(): + return super().run(*args, **kwargs) diff --git a/yafti/screen/package/screen/install.py b/yafti/screen/package/screen/install.py index 02f4ee9..94e12b6 100644 --- a/yafti/screen/package/screen/install.py +++ b/yafti/screen/package/screen/install.py @@ -9,7 +9,7 @@ from yafti import log from yafti.abc import YaftiScreen from yafti.screen.console import ConsoleScreen -from yafti.screen.package.state import STATE +from yafti.screen.package.state import PackageScreenState _xml = """\ @@ -77,6 +77,7 @@ class PackageInstallScreen(YaftiScreen, Gtk.Box): def __init__( self, + id: str, title: str = "Package Installation", package_manager: str = "yafti.plugin.flatpak", package_manager_defaults: Optional[dict] = None, @@ -89,6 +90,7 @@ def __init__( self.package_manager = PLUGINS.get(package_manager) self.package_manager_defaults = package_manager_defaults or {} self.btn_console.connect("clicked", self.toggle_console) + self.state = PackageScreenState(id) async def on_activate(self): if self.started or self.already_run: @@ -114,7 +116,7 @@ async def do_pulse(self): def draw(self): self.console.hide() self.append(self.console) - packages = [item.replace("pkg:", "") for item in STATE.get_on("pkg:")] + packages = [item.replace("pkg:", "") for item in self.state.get_on("pkg:")] asyncio.create_task(self.do_pulse()) return self.install(packages) diff --git a/yafti/screen/package/screen/package.py b/yafti/screen/package/screen/package.py index 62f68ab..930f71f 100644 --- a/yafti/screen/package/screen/package.py +++ b/yafti/screen/package/screen/package.py @@ -7,8 +7,8 @@ from yafti.abc import YaftiScreen, YaftiScreenConfig from yafti.screen.package.models import PackageConfig, PackageGroupConfig from yafti.screen.package.screen import PackageInstallScreen, PackagePickerScreen -from yafti.screen.package.state import STATE -from yafti.screen.package.utils import parse_packages +from yafti.screen.package.state import PackageScreenState +from yafti.screen.package.utils import parse_packages, generate_fingerprint _xml = """\ @@ -62,17 +62,22 @@ def __init__( self.show_terminal = show_terminal self.package_manager = package_manager self.package_manager_defaults = package_manager_defaults - STATE.load(parse_packages(self.packages)) + self.fingerprint = generate_fingerprint(self.packages) + self.state = PackageScreenState(self.fingerprint) + self.state.load(parse_packages(self.packages)) self.pkg_carousel.connect("page-changed", self.changed) self.draw() def draw(self): self.pkg_carousel.append( - PackagePickerScreen(title=self.title, packages=self.packages) + PackagePickerScreen( + id=self.fingerprint, title=self.title, packages=self.packages + ) ) self.pkg_carousel.append( PackageInstallScreen( title=self.title, + id=self.fingerprint, package_manager=self.package_manager, package_manager_defaults=self.package_manager_defaults, ) diff --git a/yafti/screen/package/screen/picker.py b/yafti/screen/package/screen/picker.py index a522923..929b715 100644 --- a/yafti/screen/package/screen/picker.py +++ b/yafti/screen/package/screen/picker.py @@ -7,7 +7,7 @@ from yafti import log from yafti.abc import YaftiScreen from yafti.screen.dialog import DialogBox -from yafti.screen.package.state import STATE +from yafti.screen.package.state import PackageScreenState from yafti.screen.utils import find_parent _xml = """\ @@ -58,6 +58,7 @@ class Config(BaseModel): def __init__( self, + id: str, title: str, packages: list | dict, **kwargs, @@ -65,6 +66,7 @@ def __init__( super().__init__(**kwargs) self.status_page.set_title(title) self.packages = packages + self.state = PackageScreenState(id) self.draw() def draw(self): @@ -76,17 +78,17 @@ def draw(self): action_row = Adw.ActionRow(title=name, subtitle=details.get("description")) def state_set(group, _, value): - STATE.set(f"group:{group}", value) + self.state.set(f"group:{group}", value) d = self.packages.get(group) for pkg in d.get("packages", []): for pkg_name in pkg.values(): if isinstance(pkg_name, dict): pkg_name = json.dumps(pkg_name) - STATE.set(f"pkg:{pkg_name}", value) + self.state.set(f"pkg:{pkg_name}", value) state_set(name, None, details.get("default", True)) _switcher = Gtk.Switch() - _switcher.set_active(STATE.get(f"group:{name}")) + _switcher.set_active(self.state.get(f"group:{name}")) _switcher.set_valign(Gtk.Align.CENTER) state_set_fn = partial(state_set, name) @@ -148,12 +150,12 @@ def _build_apps(self, packages: list): _app_switcher = Gtk.Switch() if isinstance(pkg, dict): pkg = json.dumps(pkg) - _app_switcher.set_active(STATE.get(f"pkg:{pkg}")) + _app_switcher.set_active(self.state.get(f"pkg:{pkg}")) _app_switcher.set_valign(Gtk.Align.CENTER) def set_state(pkg, btn, value): log.debug("state-set", pkg=pkg, value=value) - STATE.set(f"pkg:{pkg}", value) + self.state.set(f"pkg:{pkg}", value) set_state_func = partial(set_state, pkg) _app_switcher.connect("state-set", set_state_func) diff --git a/yafti/screen/package/state.py b/yafti/screen/package/state.py index 44f61b7..4b465a1 100644 --- a/yafti/screen/package/state.py +++ b/yafti/screen/package/state.py @@ -4,20 +4,21 @@ class PackageScreenState: __slots__ = ["state"] - @classmethod - def from_dict(cls, data: dict) -> "PackageScreenState": - self = cls() - self.load(data) - return self + def __new__(cls, id: str): + if not hasattr(cls, "instances"): + cls.instances = {} + if id not in cls.instances: + cls.instances[id] = super(PackageScreenState, cls).__new__(cls) + return cls.instances[id] + + def __init__(self, id: str): + self.state = {} @validate_arguments def load(self, data: dict): for k, v in data.items(): self.set(k, v) - def __init__(self): - self.state = {} - @validate_arguments def remove(self, item: str) -> None: del self.state[item] @@ -53,6 +54,3 @@ def keys(self) -> list[str]: @validate_arguments def get(self, item: str) -> bool: return self.state.get(item) - - -STATE = PackageScreenState() diff --git a/yafti/screen/package/utils.py b/yafti/screen/package/utils.py index 7e6a0f8..0d6a6df 100644 --- a/yafti/screen/package/utils.py +++ b/yafti/screen/package/utils.py @@ -1,4 +1,10 @@ import json +from typing import Any +from hashlib import sha256 + + +def generate_fingerprint(obj: Any): + return sha256(json.dumps(obj).encode()).hexdigest() def parse_packages(packages: dict | list) -> dict: