Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CI, tox config and other test adjustments #3

Merged
merged 26 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: test and deploy

on:
push:
branches:
- main
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
pull_request:
branches:
- main
workflow_dispatch:

concurrency:
# Concurrency group that uses the workflow name and PR number if available
# or commit SHA as a fallback. If a new build is triggered under that
# concurrency group while a previous build is running it will be canceled.
# Repeated pushes to a PR will cancel all previous builds, while multiple
# merges to main will not cancel.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

jobs:
test:
name: ${{ matrix.platform }}, py${{ matrix.python-version }}, napari ${{ matrix.napari }}
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11"]
napari: ["latest", "repo"]
exclude:
# TODO: Remove when we have a napari release with the plugin manager changes
- napari: "latest"
# TODO: PyQt / PySide wheels missing
- python-version: "3.11"
platform: "windows-latest"

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- uses: tlambert03/setup-qt-libs@v1

# strategy borrowed from vispy for installing opengl libs on windows
- name: Install Windows OpenGL
if: runner.os == 'Windows'
run: |
git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
powershell gl-ci-helpers/appveyor/install_opengl.ps1
if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools tox tox-gh-actions

- name: Test with tox
uses: aganders3/headless-gui@v1
with:
run: python -m tox -vv
env:
PYVISTA_OFF_SCREEN: True # required for opengl on windows
NAPARI: ${{ matrix.napari }}
FORCE_COLOR: 1
# PySide6 only functional with Python 3.10+
TOX_SKIP_ENV: ".*py39-PySide6.*"

- name: pre-commit
uses: pre-commit/[email protected]

- name: Coverage
uses: codecov/codecov-action@v3

deploy:
# this will run when you have tagged a commit, starting with "v*"
# and requires that you have put your twine API key in your
# github secrets (see readme for details)
needs: [test]
runs-on: ubuntu-latest
if: contains(github.ref, 'tags')
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U setuptools twine build
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
run: |
git tag
python -m build
twine upload dist/*
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![License](https://img.shields.io/pypi/l/napari-plugin-manager.svg?color=green)](https://github.com/napari/napari-plugin-manager/raw/main/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/napari-plugin-manager.svg?color=green)](https://pypi.org/project/napari-plugin-manager)
[![Python Version](https://img.shields.io/pypi/pyversions/napari-plugin-manager.svg?color=green)](https://python.org)
[![tests](https://github.com/napari/napari-plugin-manager/workflows/tests/badge.svg)](https://github.com/napari/napari-plugin-manager/actions)
[![tests](https://github.com/napari/napari-plugin-manager/workflows/test_and_deploy/badge.svg)](https://github.com/napari/napari-plugin-manager/actions)
[![codecov](https://codecov.io/gh/napari/napari-plugin-manager/branch/main/graph/badge.svg)](https://codecov.io/gh/napari/napari-plugin-manager)

A plugin that adds a plugin manager to [napari].
Expand Down
18 changes: 18 additions & 0 deletions napari_plugin_manager/_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest
from qtpy.QtWidgets import QDialog, QInputDialog, QMessageBox


@pytest.fixture(autouse=True)
def _block_message_box(monkeypatch, request):
def raise_on_call(*_, **__):
raise RuntimeError("exec_ call") # pragma: no cover

monkeypatch.setattr(QMessageBox, "exec_", raise_on_call)
monkeypatch.setattr(QMessageBox, "critical", raise_on_call)
monkeypatch.setattr(QMessageBox, "information", raise_on_call)
monkeypatch.setattr(QMessageBox, "question", raise_on_call)
monkeypatch.setattr(QMessageBox, "warning", raise_on_call)
monkeypatch.setattr(QInputDialog, "getText", raise_on_call)
# QDialogs can be allowed via a marker; only raise if not decorated
if "enabledialog" not in request.keywords:
monkeypatch.setattr(QDialog, "exec_", raise_on_call)
121 changes: 58 additions & 63 deletions napari_plugin_manager/_tests/test_qt_plugin_dialog.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import importlib.metadata
import sys
from typing import Generator, Optional, Tuple
from unittest.mock import patch

import napari.plugins
import npe2
import pytest
import qtpy
from napari.plugins._tests.test_npe2 import mock_pm # noqa
from napari.utils.translations import trans

from napari_plugin_manager import qt_plugin_dialog
from napari_plugin_manager.qt_package_installer import InstallerActions

if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] == (3, 11):
pytest.skip(
"Known PySide2 x Python 3.11 incompatibility: "
"TypeError: 'PySide2.QtCore.Qt.Alignment' object cannot be interpreted as an integer",
allow_module_level=True,
)

N_MOCKED_PLUGINS = 2


def _iter_napari_pypi_plugin_info(
conda_forge: bool = True,
Expand All @@ -35,7 +46,7 @@ def _iter_napari_pypi_plugin_info(
"author": "test author",
"license": "UNKNOWN",
}
for i in range(2):
for i in range(N_MOCKED_PLUGINS):
yield npe2.PackageMetadata(name=f"test-name-{i}", **base_data), bool(
i
), {
Expand Down Expand Up @@ -70,8 +81,32 @@ def plugins(qtbot):
return PluginsMock()


@pytest.fixture
def plugin_dialog(qtbot, monkeypatch, mock_pm, plugins, old_plugins): # noqa
class WarnPopupMock:
def __init__(self, text):
self._is_visible = False

def exec_(self):
self._is_visible = True

def move(self, pos):
return False

def isVisible(self):
return self._is_visible

def close(self):
self._is_visible = False


@pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
def plugin_dialog(
request,
qtbot,
monkeypatch,
mock_pm, # noqa
plugins,
old_plugins,
):
"""Fixture that provides a plugin dialog for a normal napari install."""

class PluginManagerMock:
Expand Down Expand Up @@ -102,16 +137,6 @@ def disable(self, plugin):
self.plugins[plugin] = False
return

class WarnPopupMock:
def __init__(self, text):
return None

def exec_(self):
return None

def move(self, pos):
return False

def mock_metadata(name):
meta = {
'version': '0.1.0',
Expand Down Expand Up @@ -145,15 +170,12 @@ def set_blocked(self, plugin, blocked):
"iter_napari_plugin_info",
_iter_napari_pypi_plugin_info,
)

monkeypatch.setattr(qt_plugin_dialog, 'WarnPopup', WarnPopupMock)

# This is patching `napari.utils.misc.running_as_constructor_app` function
# to mock a normal napari install.
monkeypatch.setattr(
qt_plugin_dialog,
"running_as_constructor_app",
lambda: False,
qt_plugin_dialog, "running_as_constructor_app", lambda: request.param
)

monkeypatch.setattr(
Expand All @@ -166,42 +188,20 @@ def set_blocked(self, plugin, blocked):

widget = qt_plugin_dialog.QtPluginDialog()
widget.show()
qtbot.wait(300)
qtbot.add_widget(widget)
yield widget
widget.hide()
widget._add_items_timer.stop()
assert not widget._add_items_timer.isActive()
qtbot.waitUntil(widget.isVisible, timeout=300)

def available_list_populated():
return widget.available_list.count() == N_MOCKED_PLUGINS

@pytest.fixture
def plugin_dialog_constructor(qtbot, monkeypatch):
"""
Fixture that provides a plugin dialog for a constructor based install.
"""
monkeypatch.setattr(
qt_plugin_dialog,
"iter_napari_plugin_info",
_iter_napari_pypi_plugin_info,
)

# This is patching `napari.utils.misc.running_as_constructor_app` function
# to mock a constructor based install.
monkeypatch.setattr(
qt_plugin_dialog,
"running_as_constructor_app",
lambda: True,
)
widget = qt_plugin_dialog.QtPluginDialog()
widget.show()
qtbot.wait(300)
qtbot.waitUntil(available_list_populated, timeout=3000)
qtbot.add_widget(widget)
yield widget
widget.hide()
widget._add_items_timer.stop()
assert not widget._add_items_timer.isActive()


def test_filter_not_available_plugins(plugin_dialog_constructor):
def test_filter_not_available_plugins(qtbot, plugin_dialog):
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
"""
Check that the plugins listed under available plugins are
enabled and disabled accordingly.
Expand All @@ -212,14 +212,14 @@ def test_filter_not_available_plugins(plugin_dialog_constructor):
The second plugin ("test-name-1") is available on conda-forge and
should be enabled without the tooltip warning.
"""
item = plugin_dialog_constructor.available_list.item(0)
widget = plugin_dialog_constructor.available_list.itemWidget(item)
item = plugin_dialog.available_list.item(0)
widget = plugin_dialog.available_list.itemWidget(item)
if widget:
assert not widget.action_button.isEnabled()
assert widget.warning_tooltip.isVisible()

item = plugin_dialog_constructor.available_list.item(1)
widget = plugin_dialog_constructor.available_list.itemWidget(item)
item = plugin_dialog.available_list.item(1)
widget = plugin_dialog.available_list.itemWidget(item)
assert widget.action_button.isEnabled()
assert not widget.warning_tooltip.isVisible()

Expand Down Expand Up @@ -253,30 +253,25 @@ def test_filter_installed_plugins(plugin_dialog):
assert plugin_dialog.installed_list._count_visible() == 0


def test_visible_widgets(plugin_dialog):
def test_visible_widgets(request, plugin_dialog):
"""
Test that the direct entry button and textbox are visible for
normal napari installs.
Test that the direct entry button and textbox are visible
"""

if "no-constructor" not in request.node.name:
# the plugin_dialog fixture has this id
# skip for 'constructor' variant
pytest.skip()
assert plugin_dialog.direct_entry_edit.isVisible()
assert plugin_dialog.direct_entry_btn.isVisible()


def test_constructor_visible_widgets(plugin_dialog_constructor):
"""
Test that the direct entry button and textbox are hidden for
constructor based napari installs.
"""
assert not plugin_dialog_constructor.direct_entry_edit.isVisible()
assert not plugin_dialog_constructor.direct_entry_btn.isVisible()


def test_version_dropdown(plugin_dialog):
def test_version_dropdown(qtbot, plugin_dialog):
"""
Test that when the source drop down is changed, it displays the other versions properly.
"""
widget = plugin_dialog.available_list.item(1).widget
qtbot.wait(300)

jaimergp marked this conversation as resolved.
Show resolved Hide resolved
assert widget.version_choice_dropdown.currentText() == "3"
# switch from PyPI source to conda one.
widget.source_choice_dropdown.setCurrentIndex(1)
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ dynamic = [
"version"
]

[project.optional-dependencies]
testing = [
"pytest",
"virtualenv",
"pytest-cov",
"pytest-qt",
"pytest-xvfb ; sys_platform == 'linux'"
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
]

[project.urls]
homepage = "https://github.com/napari/napari-plugin-manager"

Expand Down Expand Up @@ -159,6 +168,10 @@ filterwarnings = [
"error:::test_.*", # turn warnings in our own tests into errors
]

markers = [
"enabledialog: Allow to use dialog in test"
]

[tool.mypy]
files = "napari_plugin_manager"
ignore_missing_imports = true
Expand Down
Loading