diff --git a/.foundry/.gitignore b/.foundry/.gitignore index dcb888d..a30dfbf 100644 --- a/.foundry/.gitignore +++ b/.foundry/.gitignore @@ -1,3 +1,3 @@ -cache/ tmp/ user/ +cache/ diff --git a/.gitignore b/.gitignore index 590d901..3798f94 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ dev.qwery.AddWater*.json~ firefox-gnome-theme pylint-results.txt subprojects/libportal -tests/__pycache__ +__pycache__ diff --git a/meson.build b/meson.build index fece547..c9a1e53 100644 --- a/meson.build +++ b/meson.build @@ -1,13 +1,18 @@ project('addwater', version: '1.3', meson_version: '>= 1.4.0', - default_options: [ 'warning_level=2', 'werror=false', ], + default_options: ['warning_level=2', 'werror=true', + 'buildtype=debugoptimized', 'python.install_env=auto'], ) i18n = import('i18n') gnome = import('gnome') python = import('python').find_installation('python3', - modules: ['packaging', 'requests',]) + modules: [ + 'packaging', + 'requests', + ] +) pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() @@ -36,9 +41,17 @@ conf = configuration_data({ conf.set('MOCK_API', mock_api) conf.set('FORCE_BG', force_bg) -dependency('gtk4') -dependency('libadwaita-1', version: '>=1.9.0') -dependency('libportal', version: '>=0.9.1') +dependency('gtk4', version: '>=4.22') +dependency('libadwaita-1', version: '>=1.9.0') +dependency('libportal', version: '>=0.9.1', + default_options: ['backend-gtk4=enabled', + 'backend-gtk3=disabled', + 'backend-qt5=disabled', + 'backend-qt6=disabled', + 'vapi=false', + 'introspection=true', + 'werror=false']) +dependency('libportal-gtk4', version: '>=0.9.1') subdir('data') subdir('po') @@ -46,6 +59,7 @@ subdir('src') if get_option('tests') subdir('build-aux') + subdir('tests') endif gnome.post_install( diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..a896a08 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,7 @@ +iniconfig==2.3.1 +packaging==26.3 +pluggy==1.6.0 +Pygments==2.21.0 +pyproject-metadata==0.12.1 +pytest==9.1.1 +pytest-mock==3.16.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..18e5399 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +certifi==2026.7.22 +charset-normalizer==3.5.2 +idna==3.20 +packaging==26.3 +pycairo==1.29.2 +PyGObject==3.58.0 +requests==2.34.2 +urllib3==2.8.0 diff --git a/src/apps/firefox/__init__.py b/src/apps/firefox/__init__.py index ba770f1..3a43dd8 100644 --- a/src/apps/firefox/__init__.py +++ b/src/apps/firefox/__init__.py @@ -1,3 +1,2 @@ -# FIXME circular import when organized -from .firefox_paths import FirefoxPack as FirefoxPack from .firefox_details import FirefoxAppDetails as FirefoxAppDetails +from .firefox_paths import FirefoxPack as FirefoxPack diff --git a/src/apps/firefox/firefox_details.py b/src/apps/firefox/firefox_details.py index 3685ea4..195d760 100644 --- a/src/apps/firefox/firefox_details.py +++ b/src/apps/firefox/firefox_details.py @@ -24,14 +24,14 @@ from pathlib import Path from addwater import info -from addwater.profile import AppProfile -from addwater.utils import paths +from addwater.model import AppProfile +from addwater.model.paths import aw_cache_dir from gi.repository import Gio from packaging.version import Version -from . import FirefoxPack from .firefox_install import install_for_firefox from .firefox_options import FIREFOX_OPTIONS +from .firefox_paths import FirefoxPack log = logging.getLogger("firefox_details") @@ -57,12 +57,14 @@ class FirefoxAppDetails: ) # TODO just save the unzipped download in /tmp/ and then unzip into addwater/cache - save_to = paths.DOWNLOAD_DIR + # TODO make these into Paths, not PathLike strs + save_to = aw_cache_dir() / "themes" app_folder = "firefox" theme_folder = "firefox-gnome-theme" full_path = join(save_to, app_folder, theme_folder) def __init__(self) -> None: + self.save_to.mkdir(exist_ok=True, parents=False) self.settings = self.get_new_gsettings() version = Version(self.settings.get_string("installed-version")) diff --git a/src/apps/firefox/firefox_install.py b/src/apps/firefox/firefox_install.py index 7ef116d..8b09c20 100644 --- a/src/apps/firefox/firefox_install.py +++ b/src/apps/firefox/firefox_install.py @@ -25,7 +25,7 @@ from os.path import exists, join from addwater.components.install import InstallException -from addwater.profile import AppProfile +from addwater.model import AppProfile log = logging.getLogger("firefox_install") diff --git a/src/apps/firefox/firefox_paths.py b/src/apps/firefox/firefox_paths.py index 6784d35..3ffceda 100644 --- a/src/apps/firefox/firefox_paths.py +++ b/src/apps/firefox/firefox_paths.py @@ -1,59 +1,53 @@ -from enum import Enum -from os import PathLike, environ -from os.path import exists, expanduser, join +from os import PathLike +from os.path import exists, join from pathlib import Path -config_dir = environ.get("HOST_XDG_CONFIG_HOME", expanduser("~/.config")) +from addwater.model import AppPack +from addwater.model.paths import user_config, user_home + +# TODO use new paths mod # FIREFOX PATHS -# TODO replace fstring with join() -BASE_LEGACY = expanduser("~/.mozilla/firefox/") -BASE = expanduser(f"{config_dir}/mozilla/firefox/") -FLATPAK_LEGACY = expanduser("~/.var/app/org.mozilla.firefox/.mozilla/firefox/") -FLATPAK = expanduser("~/.var/app/org.mozilla.firefox/config/mozilla/firefox/") -SNAP_LEGACY = expanduser("~/snap/firefox/common/.mozilla/firefox/") -SNAP = expanduser("~/snap/firefox/common/.config/mozilla/firefox/") - -LIBREWOLF_BASE_LEGACY = expanduser("~/.librewolf/") -LIBREWOLF_BASE = expanduser(f"{config_dir}/librewolf/librewolf") -LIBREWOLF_FLATPAK_LEGACY = expanduser( - "~/.var/app/io.gitlab.librewolf-community/.librewolf/" -) -LIBREWOLF_FLATPAK = expanduser( - "~/.var/app/io.gitlab.librewolf-community/config/librewolf/librewolf/" -) - -FLOORP_BASE = expanduser("~/.floorp") -FLOORP_FLATPAK = expanduser("~/.var/app/one.ablaze.floorp/.floorp") - -WATERFOX_BASE = expanduser("~/.waterfox") -WATERFOX_FLATPAK = expanduser("~/.var/app/net.waterfox.waterfox/.waterfox") - -CACHY_BASE = expanduser("~/.cachy") +BASE_LEGACY = user_home() / ".mozilla/firefox/" +BASE = user_config() / "mozilla/firefox/" +FLATPAK_LEGACY = user_home() / ".var/app/org.mozilla.firefox/.mozilla/firefox/" +FLATPAK = user_home() / ".var/app/org.mozilla.firefox/config/mozilla/firefox/" + +SNAP_LEGACY = user_home() / "snap/firefox/common/.mozilla/firefox/" +SNAP = user_home() / "snap/firefox/common/.config/mozilla/firefox/" + +LIBREWOLF_BASE_LEGACY = user_home() / ".librewolf/" +LIBREWOLF_BASE = user_config() / "librewolf/librewolf" +LIBREWOLF_FLATPAK_LEGACY = user_home() / ".var/app/io.gitlab.librewolf-community/.librewolf/" +LIBREWOLF_FLATPAK = user_home() / ".var/app/io.gitlab.librewolf-community/config/librewolf/librewolf/" + +FLOORP_BASE = user_home() / ".floorp" +FLOORP_FLATPAK = user_home() / ".var/app/one.ablaze.floorp/.floorp" + +WATERFOX_BASE = user_home() / ".waterfox" +WATERFOX_FLATPAK = user_home() / ".var/app/net.waterfox.waterfox/.waterfox" + +CACHY_BASE = user_home() / ".cachy" # TODO can this be a GObject so I can use it in ListModels? # or must I make a wrapper? -class FirefoxPack(Enum): +class FirefoxPack(AppPack): # TODO rename these to just use the name instead of base later (Firefox / Firefox Flatpak / Librewolf / etc) - BASE = ("Base", BASE) - BASE_ALT = ("Base (Alt)", BASE_LEGACY) - FLATPAK = ("Flatpak", FLATPAK) - FLATPAK_ALT = ("Flatpak (Alt)", FLATPAK_LEGACY) - SNAP = ("Snap", SNAP) - SNAP_ALT = ("Snap (Alt)", SNAP_LEGACY) - LIBREWOLF = ("Librewolf", LIBREWOLF_BASE) - LIBREWOLF_ALT = ("Librewolf (Alt)", LIBREWOLF_BASE_LEGACY) - LIBREWOLF_FLATPAK = ("Librewolf Flatpak", LIBREWOLF_FLATPAK) + BASE = ("Base", BASE) + BASE_ALT = ("Base (Alt)", BASE_LEGACY) + FLATPAK = ("Flatpak", FLATPAK) + FLATPAK_ALT = ("Flatpak (Alt)", FLATPAK_LEGACY) + SNAP = ("Snap", SNAP) + SNAP_ALT = ("Snap (Alt)", SNAP_LEGACY) + LIBREWOLF = ("Librewolf", LIBREWOLF_BASE) + LIBREWOLF_ALT = ("Librewolf (Alt)", LIBREWOLF_BASE_LEGACY) + LIBREWOLF_FLATPAK = ("Librewolf Flatpak", LIBREWOLF_FLATPAK) LIBREWOLF_FLATPAK_ALT = ("Librewolf Flatpak (Alt)", LIBREWOLF_FLATPAK_LEGACY) - FLOORP = ("Floorp", FLOORP_BASE) - FLOORP_FLATPAK = ("Floorp Flatpak", FLOORP_FLATPAK) - WATERFOX = ("Waterfox", WATERFOX_BASE) - WATERFOX_FLATPAK = ("Waterfox Flatpak", WATERFOX_FLATPAK) - CACHY = ("CachyOS Browser", CACHY_BASE) - - def __init__(self, name: str, path: PathLike) -> None: - self.pack_name = name - self.path = Path(path) + FLOORP = ("Floorp", FLOORP_BASE) + FLOORP_FLATPAK = ("Floorp Flatpak", FLOORP_FLATPAK) + WATERFOX = ("Waterfox", WATERFOX_BASE) + WATERFOX_FLATPAK = ("Waterfox Flatpak", WATERFOX_FLATPAK) + CACHY = ("CachyOS Browser", CACHY_BASE) def get_profile_ini(self) -> Path: prof_ini = join(self.path, "profiles.ini") @@ -64,7 +58,7 @@ def get_profile_ini(self) -> Path: @staticmethod - def new_from_path(app_path: Path | PathLike) -> Enum | None: + def new_from_path(app_path: Path | PathLike) -> AppPack | None: app_path = Path(app_path) for pack in FirefoxPack: if app_path == pack.path: @@ -73,7 +67,7 @@ def new_from_path(app_path: Path | PathLike) -> Enum | None: return None @staticmethod - def new_from_name(name: str) -> Enum | None: + def new_from_name(name: str) -> AppPack | None: for pack in FirefoxPack: if name == pack.pack_name: return pack diff --git a/src/backend.py b/src/backend.py index 668f6fd..1f5b4b0 100644 --- a/src/backend.py +++ b/src/backend.py @@ -25,7 +25,7 @@ from addwater import info from addwater.apps.firefox import FirefoxAppDetails, FirefoxPack from addwater.components import InstallManager, InstallStatus, OnlineManager -from addwater.profile import AppProfile +from addwater.model import AppProfile from addwater.utils.mocks import mock_online from gi.repository import Gio from packaging.version import Version diff --git a/src/components/install.py b/src/components/install.py index cea0812..4dee6a3 100644 --- a/src/components/install.py +++ b/src/components/install.py @@ -24,7 +24,7 @@ from os import PathLike from os.path import join -from addwater.profile import AppProfile +from addwater.model import AppProfile log = logging.getLogger("install_manager") @@ -89,8 +89,8 @@ def combined_install( def uninstall(self, profile: AppProfile, folder_name: str) -> None: try: self._uninstaller(profile, folder_name) - except InstallException: - log.exception() + except InstallException as err: + log.exception(err) return InstallStatus.FAILURE return InstallStatus.SUCCESS @@ -177,8 +177,8 @@ def _do_uninstall_theme(profile: AppProfile, theme_folder: str) -> None: lines[i] = line.replace("true", "false") file.writelines(lines) - except OSError: - log.exception() + except OSError as err: + log.exception(err) raise InstallException("uninstall failed") log.info("Done.") @@ -202,8 +202,8 @@ def _remove_css_imports(chrome_path: PathLike) -> None: lines.remove(line) file.writelines(lines) - except FileNotFoundError: - log.exception() + except FileNotFoundError as err: + log.exception(err) class InstallStatus(Enum): SUCCESS = 0 diff --git a/src/components/online.py b/src/components/online.py index 868fa94..549b8dc 100644 --- a/src/components/online.py +++ b/src/components/online.py @@ -104,8 +104,8 @@ def _begin_download(self, path_info: str, tarball_url: str) -> Enum: ) except NetworkException: return OnlineStatus.DISCONNECTED - except ExtractionException: - log.exception() + except ExtractionException as err: + log.exception(err) return OnlineStatus.OTHER_ERROR # TODO handle this error better else: return OnlineStatus.UPDATED @@ -129,7 +129,7 @@ def _get_release(self, base_path: str, final_name: str, tarball_url: str) -> Non try: self._download_tarball(tarball_url, zipfile) except (requests.RequestException, requests.ConnectionError) as err: - log.exception() + log.exception(err) raise NetworkException(err) try: @@ -227,11 +227,11 @@ def _get_release_info(gh_url: str) -> dict: } try: response = requests.get(gh_url, headers=headers, timeout=10) - except requests.RequestException: + except requests.RequestException as err: # TODO use specific exceptions to handle being disconnected. It'll # be more helpful if the issue isn't just being offline but an API or # programmer error - log.exception() + log.exception(err) raise NetworkException("failed to connect to Github") api_calls_left = int(response.headers["x-ratelimit-remaining"]) @@ -239,8 +239,8 @@ def _get_release_info(gh_url: str) -> dict: latest_release = response.json()[0] version = Version(latest_release["tag_name"]) tarball_url = latest_release["tarball_url"] - except requests.JSONDecodeError: - log.exception() + except requests.JSONDecodeError as err: + log.exception(err) version = None tarball_url = None diff --git a/src/gui/pack_selector.py b/src/gui/pack_selector.py index e698e4d..64cda70 100644 --- a/src/gui/pack_selector.py +++ b/src/gui/pack_selector.py @@ -86,8 +86,8 @@ def set_package(self) -> None: if new_pack: try: self.backend.set_package(new_pack) - except InterfaceMisuseError: - log.exception() + except InterfaceMisuseError as err: + log.exception(err) self.valid_path = False else: self.valid_path = True diff --git a/src/gui/profile_selector.py b/src/gui/profile_selector.py index 913ec70..fb6f4b7 100644 --- a/src/gui/profile_selector.py +++ b/src/gui/profile_selector.py @@ -22,7 +22,7 @@ from addwater import info from addwater.apps.firefox import FirefoxPack -from addwater.profile import AppProfile +from addwater.model import AppProfile from gi.repository import Adw, Gio, GObject, Gtk log = logging.getLogger("profile_selector") diff --git a/src/main.py b/src/main.py index 082750a..a13fd16 100644 --- a/src/main.py +++ b/src/main.py @@ -31,7 +31,6 @@ from addwater.backend import Backend from gi.repository import Adw, Gio, GLib -from .utils import paths from .utils.background import BackgroundUpdater from .utils.logs import init_logs from .window import Window @@ -50,7 +49,6 @@ def __init__(self) -> None: resource_base_path=info.PREFIX, ) - paths.init_paths() init_logs() self.init_actions() @@ -127,15 +125,13 @@ def on_reset_app_action(self, *_args) -> None: settings = Gio.Settings(info.APP_ID) settings.reset("background-update") + # TODO delete theme files. Maybe add a method to tell + # AppTheme to clean up its data? Or make its dir + # public + for each in self.backends: each.reset_app() - try: - shutil.rmtree(paths.DOWNLOAD_DIR) - except FileNotFoundError: - pass - log.info("deleted download folder") - log.info("app has been reset and will now exit") self.quit() diff --git a/src/meson.build b/src/meson.build index 031c4e0..145406d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,22 +11,12 @@ configure_file( install_mode: 'r-xr-xr-x' ) -# TODO setup / teardown data to simulate profiles -# TODO test: background updater -# TODO test: installer -# TODO test: find old profiles -# TODO test: finding new profiles -# TODO test: translations with podebug -if get_option('tests') - install_subdir('tests', install_dir: moduledir) - test('pytest', find_program('pytest'), - args: [moduledir / 'tests', '--quiet']) -endif install_subdir('apps', install_dir: moduledir) install_subdir('components', install_dir: moduledir) install_subdir('utils', install_dir: moduledir) install_subdir('gui', install_dir: moduledir) +install_subdir('model', install_dir: moduledir) addwater_sources = [ '__init__.py', @@ -34,8 +24,7 @@ addwater_sources = [ 'window.py', 'page.py', 'backend.py', - 'profile.py', configure_file(input: 'info.py.in', output: 'info.py', configuration: conf), ] -install_data(addwater_sources, install_dir: moduledir) \ No newline at end of file +install_data(addwater_sources, install_dir: moduledir) diff --git a/src/model/__init__.py b/src/model/__init__.py new file mode 100644 index 0000000..9153053 --- /dev/null +++ b/src/model/__init__.py @@ -0,0 +1,2 @@ +from .package import AppPack as AppPack +from .profile import AppProfile as AppProfile diff --git a/src/model/package.py b/src/model/package.py new file mode 100644 index 0000000..9b08083 --- /dev/null +++ b/src/model/package.py @@ -0,0 +1,33 @@ +from enum import Enum +from os import PathLike +from pathlib import Path +from typing import abstractmethod + + +class AppPack(Enum): + _name: str + _path: Path + + def __init__(self, name: str, path: PathLike | Path) -> None: + self._name = name + self._path = Path(path) + + @staticmethod + @abstractmethod + def new_from_name(name: str) -> None: + raise NotImplementedError + + @staticmethod + @abstractmethod + def new_from_path(path: Path | PathLike) -> None: + raise NotImplementedError + + + @property + def pack_name(self) -> str: + return self._name + + @property + def path(self) -> Path: + return self._path + diff --git a/src/model/paths.py b/src/model/paths.py new file mode 100644 index 0000000..18fcc96 --- /dev/null +++ b/src/model/paths.py @@ -0,0 +1,84 @@ +from os import PathLike, environ +from pathlib import Path + +# Flatpak overrides XDG_DATA_HOME so we need to also check HOST_XDG_DATA_HOME +_user_data_home: Path = None # Real host data dir +_flatpak_data_home: Path | None = None # Container data dir if exists +_user_config: Path = None + +# These should always prefer container path if in one +_aw_data_dir: Path = None +_aw_config_dir: Path = None +_aw_cache_dir: Path = None + +def user_home() -> Path: + """User's HOME dir""" + if not (path := environ.get("HOME")): + raise OSError("env HOME is not set. bail out!") + + return ensure_exists(path) + +def user_data_home() -> Path: + """User's true XDG_DATA_HOME dir. To use the Flatpak sandboxed dir, use + `flatpak_data_home()` instead""" + key = "XDG_DATA_HOME" + if in_flatpak(): + key = "HOST_" + key + + if not (path := environ.get(key)): + path = "~/.local/share" + + return ensure_exists(path) + +def user_config() -> Path: + """User's true XDG_CONFIG_HOME dir""" + key = "XDG_CONFIG_HOME" + if in_flatpak(): + key = "HOST_" + key + + if not (path := environ.get(key)): + path = "~/.config" + + return ensure_exists(path) + +def aw_data_dir() -> Path: + """Add Water data dir. Always prefer sandbox dir if in one""" + if in_flatpak(): + path = flatpak_data_home() / 'addwater' + else: + path = user_data_home() / 'addwater' + + return ensure_exists(path) + +def aw_config_dir() -> Path: + """Add Water config dir. Always prefer sandbox dir if in one""" + if not (path := environ.get("XDG_CONFIG_HOME")): + path = "~/.config" + + return ensure_exists(Path(path) / 'addwater') + +def aw_cache_dir() -> Path: + """Add Water cache dir. Always prefer sandbox dir if in one""" + if not (path := environ.get("XDG_CACHE_HOME")): + path = "~/.cache" + + return ensure_exists(Path(path) / 'addwater') + +def ensure_exists(path: Path | PathLike) -> Path: + """Ensure a Path already exists in the filesystem""" + new = Path(path).expanduser() + new.mkdir(exist_ok=True, parents=True) + return new + +def flatpak_data_home() -> Path | None: + """Returns the sandboxed XDG_DATA_HOME dir. If not inside Flatpak, + returns None""" + if not in_flatpak(): + return None + + return ensure_exists(environ.get("XDG_DATA_HOME")) + +def in_flatpak() -> bool: + """Check for signs that we're inside a Flatpak sandbox""" + return environ.get("container") == "flatpak" + diff --git a/src/profile.py b/src/model/profile.py similarity index 93% rename from src/profile.py rename to src/model/profile.py index 620c194..1d1b1d2 100644 --- a/src/profile.py +++ b/src/model/profile.py @@ -19,11 +19,14 @@ from pathlib import Path -from addwater.apps.firefox import FirefoxPack from gi.repository import GObject from gi.repository.GObject import ParamFlags +from . import AppPack + +# FIXME also check for profiles in new FF profile system. +# that's the default now class AppProfile(GObject.Object): __gtype_name__ = "WaterProfile" @@ -49,7 +52,7 @@ def __init__( id: str, path: str | Path, favorite: bool, - package: FirefoxPack, + package: AppPack, ) -> None: super().__init__(name=name, id=id, favorite=favorite) self.path = Path(path) diff --git a/src/utils/logs.py b/src/utils/logs.py index 92f3343..b8f4621 100644 --- a/src/utils/logs.py +++ b/src/utils/logs.py @@ -21,27 +21,30 @@ import logging import os -import os.path import sys + from datetime import datetime, timezone +from os.path import join from addwater.info import PROFILE, VERSION +from addwater.model.paths import aw_cache_dir from gi.repository import Adw, Gtk -from . import paths - def init_logs() -> None: """Set up logging system""" - LOG_DIR = paths.LOG_DIR + LOG_DIR = aw_cache_dir() / "logs" + LOG_DIR.mkdir(exist_ok=True, parents=False) + try: now = datetime.now(timezone.utc).strftime("%Y-%m-%d") - CURRENT_LOGFILE = os.path.join(LOG_DIR, f"addwater_{now}.log") + CURRENT_LOGFILE = str(join(LOG_DIR, f"addwater_{now}.log")) file_handler = logging.FileHandler(CURRENT_LOGFILE) console_handler = logging.StreamHandler(sys.stdout) file_handler.setLevel(logging.DEBUG) + # TODO tie this an env var instead if PROFILE == "development": console_handler.setLevel(logging.DEBUG) elif PROFILE == "default": @@ -67,7 +70,7 @@ def init_logs() -> None: difference = datetime.now(timezone.utc) - time if difference.days > 7: - os.remove(os.path.join(LOG_DIR, each.name)) + os.remove(join(LOG_DIR, each.name)) info = f""" ------------------------------------------------------------------------ diff --git a/src/utils/paths.py b/src/utils/paths.py deleted file mode 100644 index 1078659..0000000 --- a/src/utils/paths.py +++ /dev/null @@ -1,53 +0,0 @@ -# paths.py -# -# Copyright 2025 Qwery -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# SPDX-License-Identifier: GPL-3.0-or-later - -# This module owns all app paths and must be used when referring to general paths like config or firefox paths - -import os -from os.path import join - -from gi.repository import GLib - -# APP PATHS -XDG_CACHE_DIR = GLib.get_user_cache_dir() -XDG_DATA_DIR = GLib.get_user_data_dir() -XDG_CONFIG_DIR = GLib.get_user_config_dir() - -APP_CACHE = join(XDG_CACHE_DIR, "add-water") -APP_CONFIG = join(XDG_CONFIG_DIR, "add-water") -APP_DATA = join(XDG_DATA_DIR, "add-water") - -DOWNLOAD_DIR = join(APP_CACHE, "downloads") -LOG_DIR = join(APP_CACHE, "logs") - - -def init_paths() -> None: - paths = [APP_CACHE, DOWNLOAD_DIR, LOG_DIR] - for each in paths: - try: - os.mkdir(path=each) - print(f"{each} directory created.") - except FileExistsError: - print(f"{each} already exists. Skipped.") - except FileNotFoundError as err: - print("Couldn't find parent dir when initializing dirs ::", err) - return - - print("All paths initialized.") - print("-------------------------") diff --git a/subprojects/libportal.wrap b/subprojects/libportal.wrap index a9c4e19..3e4150c 100644 --- a/subprojects/libportal.wrap +++ b/subprojects/libportal.wrap @@ -2,4 +2,9 @@ directory = libportal url = https://github.com/flatpak/libportal.git revision = 8f5dc8d192f6e31dafe69e35219e3b707bde71ce -depth = 1 \ No newline at end of file +depth = 1 + +[provide] +dependency_names = libportal, libportal-gtk4 +libportal = libportal_dep +libportal-gtk4 = libportal_gtk4_dep diff --git a/src/tests/__init__.py b/tests/__init__.py similarity index 100% rename from src/tests/__init__.py rename to tests/__init__.py diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..e67e709 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,24 @@ +# TODO setup / teardown data to simulate profiles +# TODO test: background updater +# TODO test: installer +# TODO test: find old profiles +# TODO test: finding new profiles +# TODO test: translations with podebug + +configure_file( + input: 'suite.py.in', + output: 'suite.py', + configuration: conf, +) + +test_src = [ + '__init__.py', + 'test_paths.py', +] + +foreach testfile: test_src + configure_file(input: testfile, output: testfile, copy: true) +endforeach + +test('pytest', find_program('pytest'), + args: [meson.current_build_dir(), '--quiet']) diff --git a/tests/suite.py.in b/tests/suite.py.in new file mode 100644 index 0000000..8c4ba7b --- /dev/null +++ b/tests/suite.py.in @@ -0,0 +1,13 @@ +import gettext +import locale +import signal +import sys + +pkgdatadir = '@pkgdatadir@' +localedir = '@localedir@' + +sys.path.insert(1, pkgdatadir) +signal.signal(signal.SIGINT, signal.SIG_DFL) +locale.bindtextdomain('addwater', localedir) +locale.textdomain('addwater') +gettext.install('addwater', localedir) diff --git a/tests/test_paths.py b/tests/test_paths.py new file mode 100644 index 0000000..76d6ff5 --- /dev/null +++ b/tests/test_paths.py @@ -0,0 +1,112 @@ +import os +import pytest +import shutil +import sys + +from os.path import exists +from pathlib import Path + +from . import suite +sys.path.insert(1, suite.pkgdatadir) + +from addwater.model.paths import * + +TEST_ROOT = Path("/tmp/addwater/test") +TEST_HOME = TEST_ROOT / "home" + +@pytest.fixture +def fake_env(monkeypatch) -> None: + TEST_HOME.mkdir(exist_ok=False, parents=True) + + monkeypatch.setenv("HOME", str(TEST_HOME)) + monkeypatch.delenv("container") + + monkeypatch.setenv("XDG_CACHE_HOME", str(TEST_HOME / ".thecachedir")) + monkeypatch.setenv("XDG_CONFIG_HOME", str(TEST_HOME / ".theconfigurationdir")) + monkeypatch.setenv("XDG_DATA_HOME", str(TEST_HOME / ".thedatadir")) + + yield None + + shutil.rmtree(TEST_ROOT) + +@pytest.fixture +def fake_env_flatpak(monkeypatch) -> None: + TEST_HOME.mkdir(exist_ok=False, parents=True) + + SANDBOX_HOME = TEST_HOME / ".var/app/dev.qwery.AddWater" + + monkeypatch.setenv("HOME", str(TEST_HOME)) + monkeypatch.setenv("container", "flatpak") + + monkeypatch.setenv("XDG_CACHE_HOME", str(SANDBOX_HOME / ".thecachedir")) + monkeypatch.setenv("XDG_CONFIG_HOME", str(SANDBOX_HOME / ".theconfigurationdir")) + monkeypatch.setenv("XDG_DATA_HOME", str(SANDBOX_HOME / "thedatadir")) + + monkeypatch.setenv("HOST_XDG_CACHE_HOME", str(TEST_HOME / ".thecachedir")) + monkeypatch.setenv("HOST_XDG_CONFIG_HOME", str(TEST_HOME / ".theconfigurationdir")) + monkeypatch.setenv("HOST_XDG_DATA_HOME", str(TEST_HOME / ".thedatadir")) + + yield None + + shutil.rmtree(TEST_ROOT) + +@pytest.fixture +def fake_env_empty(monkeypatch) -> None: + TEST_HOME.mkdir(exist_ok=False, parents=True) + + monkeypatch.setenv("HOME", str(TEST_HOME)) + monkeypatch.delenv("container", raising=False) + + monkeypatch.delenv("XDG_CACHE_HOME", raising=False) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + + monkeypatch.delenv("HOST_XDG_CACHE_HOME", raising=False) + monkeypatch.delenv("HOST_XDG_CONFIG_HOME", raising=False) + monkeypatch.delenv("HOST_XDG_DATA_HOME", raising=False) + + yield None + + shutil.rmtree(TEST_ROOT) + +def test_paths(fake_env) -> None: + USER_CACHE = Path(environ["XDG_CACHE_HOME"]) + USER_CONFIG = Path(environ["XDG_CONFIG_HOME"]) + USER_DATA = Path(environ["XDG_DATA_HOME"]) + + assert not in_flatpak() + assert flatpak_data_home() is None + assert user_config() == USER_CONFIG + assert user_data_home() == USER_DATA + assert aw_cache_dir() == USER_CACHE / "addwater" + assert aw_config_dir() == USER_CONFIG / "addwater" + assert aw_data_dir() == USER_DATA / "addwater" + +def test_paths_in_flatpak(fake_env_flatpak) -> None: + USER_CONFIG = Path(environ["HOST_XDG_CONFIG_HOME"]) + USER_DATA = Path(environ["HOST_XDG_DATA_HOME"]) + FLATPAK_CACHE = Path(environ["XDG_CACHE_HOME"]) + FLATPAK_CONFIG = Path(environ["XDG_CONFIG_HOME"]) + FLATPAK_DATA = Path(environ["XDG_DATA_HOME"]) + + assert in_flatpak() + assert flatpak_data_home() == FLATPAK_DATA + assert user_data_home() == USER_DATA + assert user_config() == USER_CONFIG + assert aw_cache_dir() == FLATPAK_CACHE / "addwater" + assert aw_config_dir() == FLATPAK_CONFIG / "addwater" + assert aw_data_dir() == FLATPAK_DATA / "addwater" + +def test_paths_xdg_default(fake_env_empty) -> None: + HOME = Path(environ["HOME"]) + USER_CACHE = HOME / ".cache" + USER_CONFIG = HOME / ".config" + USER_DATA = HOME / ".local/share" + + assert not in_flatpak() + assert flatpak_data_home() is None + assert user_data_home() == USER_DATA + assert user_config() == USER_CONFIG + assert aw_cache_dir() == USER_CACHE / "addwater" + assert aw_data_dir() == USER_DATA / "addwater" +