diff --git a/.github/workflows/test_plugin.yml b/.github/workflows/test_plugin.yml new file mode 100644 index 0000000..03b0d77 --- /dev/null +++ b/.github/workflows/test_plugin.yml @@ -0,0 +1,31 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Run tests + +on: + push: + pull_request: + branches: + - 'main' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Pull qgis image + run: docker pull qgis/qgis:stable + + - name: Pip install + run: | + docker run --name qgis_container --volume $(pwd):/app -w=/app qgis/qgis:stable sh -c "python3 -m pip install pytest-qgis --break-system-packages" + docker commit qgis_container qgis_with_deps + + - name: Run tests + run: docker run --volume $(pwd):/app -w=/app qgis_with_deps sh -c "xvfb-run -s '+extension GLX -screen 0 1024x768x24' python3 -m pytest tests -s" diff --git a/a00_qpip/plugin.py b/a00_qpip/plugin.py index 963a9e8..837c66c 100644 --- a/a00_qpip/plugin.py +++ b/a00_qpip/plugin.py @@ -20,15 +20,18 @@ class Plugin: """QGIS Plugin Implementation.""" - def __init__(self, iface): + def __init__(self, iface, plugin_path=None): self.iface = iface self._defered_packages = [] self.settings = QgsSettings() self.settings.beginGroup("QPIP") - self.plugins_path = os.path.join( - QgsApplication.qgisSettingsDirPath(), "python", "plugins" - ) + if plugin_path is None: + self.plugins_path = os.path.join( + QgsApplication.qgisSettingsDirPath(), "python", "plugins" + ) + else: + self.plugins_path = plugin_path self.prefix_path = os.path.join( QgsApplication.qgisSettingsDirPath().replace("/", os.path.sep), "python", @@ -70,9 +73,10 @@ def initGui(self): def initComplete(self): if self._defered_packages: log(f"Initialization complete. Loading deferred packages") - self.check_deps_and_prompt_install( - additional_plugins=self._defered_packages - ) + dialog, run_gui = self.check_deps(additional_plugins=self._defered_packages) + if run_gui: + self.promt_install(dialog) + self.save_settings(dialog) self.start_packages(self._defered_packages) self._defered_packages = [] @@ -91,7 +95,9 @@ def unload(self): os.environ["PYTHONPATH"] = os.environ["PYTHONPATH"].replace( self.bin_path + os.pathsep, "" ) - os.environ["PATH"] = os.environ["PATH"].replace(self.bin_path + os.pathsep, "") + os.environ["PATH"] = os.environ["PATH"].replace( + self.bin_path + os.pathsep, "" + ) def patched_load_plugin(self, packageName): """ @@ -118,14 +124,21 @@ def patched_load_plugin(self, packageName): return self._original_loadPlugin(packageName) else: log(f"Check on install enabled, we check {packageName}.") - self.check_deps_and_prompt_install(additional_plugins=[packageName]) + dialog, run_gui = self.check_deps(additional_plugins=[packageName]) + if run_gui: + self.promt_install(dialog) + self.save_settings(dialog) self.start_packages([packageName]) return True - def check_deps_and_prompt_install(self, additional_plugins=[], force_gui=False): + def check_deps(self, additional_plugins=[]) -> MainDialog | bool: """ This checks dependencies for installed plugins and to-be installed plugins. If anything is missing, shows a GUI to install them. + + The function returns: + - MainDialog, the QDialog object (without opening it) + - A bool if the dialog needs to be opened or not """ plugin_names = [*qgis.utils.active_plugins, *additional_plugins] @@ -165,31 +178,34 @@ def check_deps_and_prompt_install(self, additional_plugins=[], force_gui=False): req = Req(plugin_name, str(requirement), error) libs[requirement.key].name = requirement.key libs[requirement.key].required_by.append(req) + dialog = MainDialog( + libs.values(), self._check_on_startup(), self._check_on_install() + ) + return dialog, needs_gui + + def promt_install(self, dialog: MainDialog): + """Promts the install dialog and ask the user what to install""" + if dialog.exec_(): + reqs_to_uninstall = dialog.reqs_to_uninstall + if reqs_to_uninstall: + log(f"Will uninstall selected dependencies : {reqs_to_uninstall}") + self.pip_uninstall_reqs(reqs_to_uninstall) + + reqs_to_install = dialog.reqs_to_install + if reqs_to_install: + log(f"Will install selected dependencies : {reqs_to_install}") + self.pip_install_reqs(reqs_to_install) + + def save_settings(self, dialog): + """Stores the settings values""" + sys.path_importer_cache.clear() - if force_gui or needs_gui: - dialog = MainDialog( - libs.values(), self._check_on_startup(), self._check_on_install() - ) - if dialog.exec_(): - reqs_to_uninstall = dialog.reqs_to_uninstall - if reqs_to_uninstall: - log(f"Will uninstall selected dependencies : {reqs_to_uninstall}") - self.pip_uninstall_reqs(reqs_to_uninstall) - - reqs_to_install = dialog.reqs_to_install - if reqs_to_install: - log(f"Will install selected dependencies : {reqs_to_install}") - self.pip_install_reqs(reqs_to_install) - - sys.path_importer_cache.clear() - - # Save these even if the dialog was closed - self.settings.setValue( - "check_on_startup", "yes" if dialog.check_on_startup else "no" - ) - self.settings.setValue( - "check_on_install", "yes" if dialog.check_on_install else "no" - ) + self.settings.setValue( + "check_on_startup", "yes" if dialog.check_on_startup else "no" + ) + self.settings.setValue( + "check_on_install", "yes" if dialog.check_on_install else "no" + ) def start_packages(self, packageNames): """ @@ -249,7 +265,9 @@ def pip_install_reqs(self, reqs_to_install): ) def check(self): - self.check_deps_and_prompt_install(force_gui=True) + dialog, _ = self.check_deps() + self.promt_install(dialog) + self.save_settings(dialog) def show_folder(self): if platform.system() == "Windows": diff --git a/tests/test_finding_req.py b/tests/test_finding_req.py new file mode 100644 index 0000000..8642511 --- /dev/null +++ b/tests/test_finding_req.py @@ -0,0 +1,43 @@ +import os + +import pytest + +from a00_qpip.plugin import Plugin + + +class initializationCompleted: + def connect(self): + pass + + +def popWidget(): + return True + + +THIS_DIR = os.path.dirname(__file__) + + +@pytest.fixture() +def plugin(qgis_iface): + qgis_iface.initializationCompleted = initializationCompleted + qgis_iface.messageBar().popWidget = popWidget + plugin = Plugin(qgis_iface, ".") + yield plugin + + +def test_plugin_a(plugin: Plugin): + plugin_a = os.path.join(THIS_DIR, "..", "test_plugins", "plugin_a") + dialog, needs_gui = plugin.check_deps([plugin_a]) + libs = dialog.reqs_to_install + assert len(libs) == 2 + assert libs[0] == "cowsay==4.0" + assert needs_gui + + +def test_plugin_b(plugin: Plugin): + plugin_b = os.path.join(THIS_DIR, "..", "test_plugins", "plugin_b") + dialog, needs_gui = plugin.check_deps([plugin_b]) + libs = dialog.reqs_to_install + assert len(libs) == 2 + assert libs[0] == "cowsay==5.0" + assert needs_gui