From d9a015aac433ea6c06ac28c10496e19814878733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Sun, 1 Dec 2024 23:42:25 -0500 Subject: [PATCH 1/4] Add initial package structure --- .gitignore | 119 ++++++++++++ .pre-commit-config.yaml | 30 +++ LICENSE.txt | 29 +++ RELEASE.md | 21 ++ napari_update_checker/napari.yaml | 11 ++ napari_update_checker/qt_update_checker.py | 175 +++++++++++++++++ pyproject.toml | 213 +++++++++++++++++++++ tox.ini | 47 +++++ 8 files changed, 645 insertions(+) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE.txt create mode 100644 RELEASE.md create mode 100644 napari_update_checker/napari.yaml create mode 100644 napari_update_checker/qt_update_checker.py create mode 100644 pyproject.toml create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e822007 --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +.napari_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask instance folder +instance/ + +# Sphinx documentation +docs/_build/ + +# MkDocs documentation +/site/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# OS +.DS_Store + +# written by setuptools_scm +*/_version.py + +# pycharm stuff +.idea/ + +# ruff stuff +.ruff_cache/ + +# spyder stuff +.spyproject/ + +# Environment variables file +.env + +# Sphinx documentation +docs/_build/ + +# MkDocs documentation +/site/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# OS +.DS_Store + +# written by setuptools_scm +*/_version.py + +# vscode +.vscode + +# Images +docs/images/ + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8a98655 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/MarcoGorelli/absolufy-imports + rev: v0.3.1 + hooks: + - id: absolufy-imports + - repo: https://github.com/hadialqattan/pycln + rev: v2.4.0 + hooks: + - id: pycln + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.10.0 + hooks: + - id: black + pass_filenames: true + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.2 + hooks: + - id: ruff + - repo: https://github.com/seddonym/import-linter + rev: v2.1 + hooks: + - id: import-linter + stages: [manual] + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.29.4 + hooks: + - id: check-github-workflows + +ci: + autoupdate_schedule: monthly diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..11d7edc --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Napari +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..b23115a --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,21 @@ +# Release Procedure + +Here you can find some information about how to trigger a new release over PyPI and subsequent `conda-forge` update. + +## PyPI + +To release on PyPI you will need to create a new tag. To do so you can: + +* Create a [new GitHub release](https://github.com/napari/update-checker/releases/new) +* Use over the new release GitHub page the `Choose a tag` dropdown to create a new tag (it should be something like `vX.Y.Z` incrementing the major, minor or patch number as required). +* Once the tag is defined you should be able to click `Generate release notes`. +* Put as release title the tag that was created (`vX.Y.Z`). +* Publish the release, check that the deploy step was run successfully and that the new version is available at [PyPI](https://pypi.org/project/napari-update-checker/#history) + +## conda-forge + +To update the `conda-forge` package you will need to update the [`napari-update-checker` feedstock](https://github.com/conda-forge/napari-update-checker-feedstock). **If a new version is already available from PyPI**, you can either wait for the automated PR or trigger one manually: + +* Create an issue over the feedstock with the title: [`@conda-forge-admin, please update version`](https://conda-forge.org/docs/maintainer/infrastructure/#conda-forge-admin-please-update-version) +* Tweak the generated PR if necessary (dependencies changes for example). +* Merge the generated PR. diff --git a/napari_update_checker/napari.yaml b/napari_update_checker/napari.yaml new file mode 100644 index 0000000..6e456df --- /dev/null +++ b/napari_update_checker/napari.yaml @@ -0,0 +1,11 @@ +name: napari-update-checker +display_name: napari update-checker +contributions: + commands: + - id: napari-update-checker.UpdateChecker + python_name: napari_update_checker.qt_update_checker:UpdateChecker + title: napari-update-checker.UpdateChecker + + widgets: + - command: napari-update-checker.UpdateChecker + display_name: Check updates diff --git a/napari_update_checker/qt_update_checker.py b/napari_update_checker/qt_update_checker.py new file mode 100644 index 0000000..9ddcb22 --- /dev/null +++ b/napari_update_checker/qt_update_checker.py @@ -0,0 +1,175 @@ +import json +import os +import sys +from concurrent.futures import ThreadPoolExecutor +from contextlib import suppress +from datetime import date +from functools import lru_cache +from urllib.error import HTTPError, URLError +from urllib.request import urlopen + +import packaging +import packaging.version +from napari import __version__ +from napari._qt.qthreading import create_worker +from napari.utils.notifications import show_warning +from qtpy.QtCore import QTimer +from qtpy.QtWidgets import ( + QLabel, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget, +) +from superqt import ensure_main_thread + +from napari_plugin_manager.qt_plugin_dialog import ON_BUNDLE + +IGNORE_DAYS = 21 +IGNORE_FILE = "ignore.txt" + + +@lru_cache +def github_tags(): + url = 'https://api.github.com/repos/napari/napari/tags' + with urlopen(url) as r: + data = json.load(r) + + versions = [] + for item in data: + version = item.get('name', None) + if version: + if version.startswith('v'): + version = version[1:] + + versions.append(version) + + return list(reversed(versions)) + + +@lru_cache +def conda_forge_releases(): + url = 'https://api.anaconda.org/package/conda-forge/napari/' + with urlopen(url) as r: + data = json.load(r) + versions = data.get('versions', []) + return versions + + +def get_latest_version(): + """Check latest version between tags and conda forge.""" + try: + with ThreadPoolExecutor() as executor: + tags = executor.submit(github_tags) + cf = executor.submit(conda_forge_releases) + + gh_tags = tags.result() + cf_versions = cf.result() + except (HTTPError, URLError): + show_warning( + 'Plugin manager: There seems to be an issue with network connectivity. ' + ) + return + + latest_version = packaging.version.parse(cf_versions[-1]) + latest_tag = packaging.version.parse(gh_tags[-1]) + if latest_version > latest_tag: + yield latest_version + else: + yield latest_tag + + +class UpdateChecker(QWidget): + + FIRST_TIME = False + URL_PACKAGE = "https://napari.org/dev/tutorials/fundamentals/installation.html#install-as-python-package-recommended" + URL_BUNDLE = "https://napari.org/dev/tutorials/fundamentals/installation.html#install-as-a-bundled-app" + + def __init__(self, parent=None): + super().__init__(parent=parent) + self._current_version = packaging.version.parse(__version__) + self._latest_version = None + self._worker = None + self._base_folder = sys.prefix + + self.label = QLabel("Checking for updates...
") + self.check_updates_button = QPushButton("Check for updates") + self.check_updates_button.clicked.connect(self._check) + + layout = QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.check_updates_button) + self.setLayout(layout) + + self._timer = QTimer() + self._timer.setInterval(2000) + self._timer.timeout.connect(self.check) + self._timer.setSingleShot(True) + self._timer.start() + + def _check(self): + self.label.setText("Checking for updates...\n") + self._timer.start() + + def check(self): + if os.path.exists(os.path.join(self._base_folder, IGNORE_FILE)): + with ( + open( + os.path.join(self._base_folder, IGNORE_FILE), + encoding="utf-8", + ) as f_p, + suppress(ValueError), + ): + old_date = date.fromisoformat(f_p.read()) + if (date.today() - old_date).days < IGNORE_DAYS: + return + + os.remove(os.path.join(self._base_folder, IGNORE_FILE)) + + self._worker = create_worker(get_latest_version) + self._worker.yielded.connect(self.show_version_info) + self._worker.start() + + @ensure_main_thread + def show_version_info(self, latest_version): + my_version = self._current_version + remote_version = latest_version + if remote_version > my_version: + url = self.URL_BUNDLE if ON_BUNDLE else self.URL_PACKAGE + msg = ( + f"You use outdated version of napari.

" + f"Installed version: {my_version}
" + f"Current version: {remote_version}

" + "For more information on how to update
" + f'visit the online documentation

' + ) + self.label.setText(msg) + message = QMessageBox( + QMessageBox.Icon.Information, + "New release", + msg, + QMessageBox.StandardButton.Ok + | QMessageBox.StandardButton.Ignore, + ) + if message.exec_() == QMessageBox.StandardButton.Ignore: + os.makedirs(self._base_folder, exist_ok=True) + with open( + os.path.join(self._base_folder, IGNORE_FILE), + "w", + encoding="utf-8", + ) as f_p: + f_p.write(date.today().isoformat()) + else: + msg = ( + f"You are using the latest version of napari!

" + f"Installed version: {my_version}

" + ) + self.label.setText(msg) + + +if __name__ == '__main__': + from qtpy.QtWidgets import QApplication + + app = QApplication([]) + checker = UpdateChecker() + sys.exit(app.exec_()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..62e12ac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,213 @@ +[build-system] +requires = [ + "setuptools >= 42", + "wheel", + "setuptools_scm[toml]>=3.4" +] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "napari_update_checker/_version.py" + +[tool.setuptools.packages.find] +where = ["."] +include = ["napari_update_checker"] +exclude = [] +namespaces = false + +[project] +name = "napari-update-checker" +description = "Updates checker plugin for napari." +readme = "README.md" +authors = [ + {name = "napari team", email = "napari-steering-council@googlegroups.com"} +] +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: X11 Applications :: Qt", + "Framework :: napari", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Utilities", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS" +] +requires-python = ">=3.8" +dependencies = [ + "npe2", + "qtpy", + "superqt", + "pip" +] +dynamic = [ + "version" +] + +[project.optional-dependencies] +dev = [ + "PyQt5", + "pre-commit", +] + +testing = [ + "flaky", + "pytest", + "pytest-cov", + "pytest-qt", + "virtualenv" +] + +docs = [ + "sphinx>6", + "sphinx-autobuild", + "sphinx-external-toc", + "sphinx-copybutton", + "sphinx-favicon", + "myst-nb", + "napari-sphinx-theme>=0.3.0", +] + +[project.urls] +homepage = "https://github.com/napari/napari-plugin-manager" + +[tool.black] +target-version = ['py39', 'py310', 'py311'] +skip-string-normalization = true +line-length = 79 + +[tool.check-manifest] +ignore = [ + ".pre-commit-config.yaml", + "napari_update_checker/_version.py", # added during build by setuptools_scm + "*.pyi", # added by make typestubs +] + +[tool.ruff] +line-length = 79 +select = [ + "E", "F", "W", #flake8 + "UP", # pyupgrade + "I", # isort + "YTT", #flake8-2020 + "TCH", # flake8-type-checing + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "COM", # flake8-commas + "SIM", # flake8-simplify + "INP", # flake8-no-pep420 + "Q", # flake8-quotes + "RET", # flake8-return + "TID", # flake8-tidy-imports # replace absolutify import + "TRY", # tryceratops + "ICN", # flake8-import-conventions + "RUF", # ruff specyfic rules +] +ignore = [ + "E501", "UP006", "TCH001", "TCH002", "TCH003", + "A003", # flake8-builtins - we have class attributes violating these rule + "COM812", # flake8-commas - we don't like adding comma on single line of arguments + "SIM117", # flake8-simplify - we some of merged with statements are not looking great with black, reanble after drop python 3.9 + "Q000", + "RET504", # not fixed yet https://github.com/charliermarsh/ruff/issues/2950 + "TRY003", # require implement multiple exception class + "RUF005", # problem with numpy compatybility, see https://github.com/charliermarsh/ruff/issues/2142#issuecomment-1451038741 + +] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".mypy_cache", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "*vendored*", + "*_vendor*", +] + +target-version = "py38" +fix = true + +[tool.ruff.per-file-ignores] +"**/_tests/*.py" = ["B011", "INP001", "TRY301"] + +[tool.ruff.flake8-quotes] +docstring-quotes = "double" + +[tool.ruff.flake8-tidy-imports] +# Disallow all relative imports. +ban-relative-imports = "all" + +[tool.ruff.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true + +[tool.ruff.isort] +known-first-party=['napari_update_checker'] + +[tool.pytest.ini_options] +# These follow standard library warnings filters syntax. See more here: +# https://docs.python.org/3/library/warnings.html#describing-warning-filters +addopts = "--maxfail=5 --durations=10 -rXxs" + +# NOTE: only put things that will never change in here. +# napari deprecation and future warnings should NOT go in here. +# instead... assert the warning with `pytest.warns()` in the relevant test, +# That way we can clean them up when no longer necessary +filterwarnings = [ + "error:::napari_update_checker", # turn warnings from napari into errors + "error:::test_.*", # turn warnings in our own tests into errors +] + +markers = [ + "enabledialog: Allow to use dialog in test" +] + +[tool.mypy] +files = "napari_update_checker" +ignore_missing_imports = true +exclude = [ + "_tests", +] +show_error_codes = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +check_untyped_defs = true +# # maybe someday :) +# disallow_any_generics = true +# no_implicit_reexport = true +# disallow_untyped_defs = true + +[project.entry-points."napari.manifest"] +napari_update_checker = "napari_update_checker:napari.yaml" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..dd8355a --- /dev/null +++ b/tox.ini @@ -0,0 +1,47 @@ +# For more information about tox, see https://tox.readthedocs.io/en/latest/ +[tox] +envlist = py{39,310,311}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{latest,repo} +toxworkdir=/tmp/.tox +isolated_build = true + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + +[gh-actions:env] +NAPARI = + latest: napari_latest + repo: napari_repo +BACKEND = + pyqt: PyQt5 + pyside: PySide2 + PyQt5: PyQt5 + PySide2: PySide2 + PyQt6: PyQt6 + PySide6: PySide6 + +[testenv] +passenv = + QT_API + CI + GITHUB_ACTIONS + AZURE_PIPELINES + DISPLAY + XAUTHORITY + NUMPY_EXPERIMENTAL_ARRAY_FUNCTION + PYVISTA_OFF_SCREEN +deps = + PyQt5: PyQt5!=5.15.0 + PyQt5: PyQt5-sip!=12.12.0 + PySide2: PySide2!=5.15.0 + PyQt6: PyQt6 + # fix PySide6 when a new napari release is out + PySide6: PySide6 != 6.4.3, !=6.5.0, !=6.5.1, !=6.5.1.1, !=6.5.2, != 6.5.3, != 6.6.0, != 6.6.1, != 6.6.2 ; python_version >= '3.10' and python_version < '3.12' + PySide2: npe2!=0.2.2 + napari_repo: git+https://github.com/napari/napari.git + napari_latest: napari +extras = testing +commands = pytest -v --color=yes --cov=napari_update_checker --cov-report=xml From 4e8b07840b14d3f69e4b089c7a1a85bc144d564d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Sun, 1 Dec 2024 23:47:46 -0500 Subject: [PATCH 2/4] Add init file --- napari_update_checker/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 napari_update_checker/__init__.py diff --git a/napari_update_checker/__init__.py b/napari_update_checker/__init__.py new file mode 100644 index 0000000..e69de29 From 72008d83d3b61867687da00c58a05114193e9693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Fri, 13 Dec 2024 18:05:23 -0500 Subject: [PATCH 3/4] Onlyt display msgbox on if not snoozed --- napari_update_checker/qt_update_checker.py | 46 +++++++++++++--------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/napari_update_checker/qt_update_checker.py b/napari_update_checker/qt_update_checker.py index 9ddcb22..11c594e 100644 --- a/napari_update_checker/qt_update_checker.py +++ b/napari_update_checker/qt_update_checker.py @@ -12,6 +12,7 @@ import packaging.version from napari import __version__ from napari._qt.qthreading import create_worker +from napari.utils.misc import running_as_constructor_app from napari.utils.notifications import show_warning from qtpy.QtCore import QTimer from qtpy.QtWidgets import ( @@ -23,8 +24,7 @@ ) from superqt import ensure_main_thread -from napari_plugin_manager.qt_plugin_dialog import ON_BUNDLE - +ON_BUNDLE = running_as_constructor_app() IGNORE_DAYS = 21 IGNORE_FILE = "ignore.txt" @@ -91,6 +91,7 @@ def __init__(self, parent=None): self._latest_version = None self._worker = None self._base_folder = sys.prefix + self._snoozed = False self.label = QLabel("Checking for updates...
") self.check_updates_button = QPushButton("Check for updates") @@ -111,7 +112,8 @@ def _check(self): self.label.setText("Checking for updates...\n") self._timer.start() - def check(self): + def _check_time(self): + # print(os.path.join(self._base_folder, IGNORE_FILE)) if os.path.exists(os.path.join(self._base_folder, IGNORE_FILE)): with ( open( @@ -121,11 +123,16 @@ def check(self): suppress(ValueError), ): old_date = date.fromisoformat(f_p.read()) + self._snoozed = (date.today() - old_date).days < IGNORE_DAYS if (date.today() - old_date).days < IGNORE_DAYS: - return + return True os.remove(os.path.join(self._base_folder, IGNORE_FILE)) + return False + + def check(self): + self._check_time() self._worker = create_worker(get_latest_version) self._worker.yielded.connect(self.show_version_info) self._worker.start() @@ -144,21 +151,22 @@ def show_version_info(self, latest_version): f'visit the online documentation

' ) self.label.setText(msg) - message = QMessageBox( - QMessageBox.Icon.Information, - "New release", - msg, - QMessageBox.StandardButton.Ok - | QMessageBox.StandardButton.Ignore, - ) - if message.exec_() == QMessageBox.StandardButton.Ignore: - os.makedirs(self._base_folder, exist_ok=True) - with open( - os.path.join(self._base_folder, IGNORE_FILE), - "w", - encoding="utf-8", - ) as f_p: - f_p.write(date.today().isoformat()) + if not self._snoozed: + message = QMessageBox( + QMessageBox.Icon.Information, + "New release", + msg, + QMessageBox.StandardButton.Ok + | QMessageBox.StandardButton.Ignore, + ) + if message.exec_() == QMessageBox.StandardButton.Ignore: + os.makedirs(self._base_folder, exist_ok=True) + with open( + os.path.join(self._base_folder, IGNORE_FILE), + "w", + encoding="utf-8", + ) as f_p: + f_p.write(date.today().isoformat()) else: msg = ( f"You are using the latest version of napari!

" From ceb7ed0aa4c8fb8941ba4c5c7e24f74c391591b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Fri, 13 Dec 2024 18:32:17 -0500 Subject: [PATCH 4/4] Add actions and testing --- .github/dependabot.yml | 10 ++ .github/workflows/deploy_docs.yml | 60 ++++++++++ .github/workflows/test_and_deploy.yml | 106 ++++++++++++++++++ napari_update_checker/_tests/__init__.py | 0 .../_tests/test_qt_update_checker.py | 0 napari_update_checker/_tests/test_utils.py | 11 ++ napari_update_checker/utils.py | 30 +++++ 7 files changed, 217 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/deploy_docs.yml create mode 100644 .github/workflows/test_and_deploy.yml create mode 100644 napari_update_checker/_tests/__init__.py create mode 100644 napari_update_checker/_tests/test_qt_update_checker.py create mode 100644 napari_update_checker/_tests/test_utils.py create mode 100644 napari_update_checker/utils.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2390d8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 0000000..c8eda6b --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,60 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + tags: + - 'v*' + workflow_dispatch: + +# Only allow one docs build at a time so that overlapping stale builds will get +# cancelled automatically. +concurrency: + group: deploy_docs + cancel-in-progress: true + +jobs: + build-and-deploy: + name: Build & Deploy + runs-on: ubuntu-latest + + permissions: + contents: write # so we can write to github pages without a token + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + steps: + - name: Clone repo + uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - uses: tlambert03/setup-qt-libs@v1 + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install "napari[all]" + python -m pip install -e ".[docs]" + + - name: Build Docs + uses: aganders3/headless-gui@v2 + with: + run: make docs + + - name: Check file tree contents + run: tree + + # At a minimum this job should upload artifacts using actions/upload-pages-artifact + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + name: github-pages + path: docs/_build + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml new file mode 100644 index 0000000..6f53b01 --- /dev/null +++ b/.github/workflows/test_and_deploy.yml @@ -0,0 +1,106 @@ +# 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-13] + 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@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + 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@v2 + 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: Coverage + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + 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') + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools build + - name: Build + run: | + git tag + python -m build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/napari_update_checker/_tests/__init__.py b/napari_update_checker/_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/napari_update_checker/_tests/test_qt_update_checker.py b/napari_update_checker/_tests/test_qt_update_checker.py new file mode 100644 index 0000000..e69de29 diff --git a/napari_update_checker/_tests/test_utils.py b/napari_update_checker/_tests/test_utils.py new file mode 100644 index 0000000..3fbd57e --- /dev/null +++ b/napari_update_checker/_tests/test_utils.py @@ -0,0 +1,11 @@ +from napari_update_checker.utils import conda_forge_releases, github_tags + + +def test_github_tags(): + data = github_tags() + assert '0.5.0a1' in data + + +def test_conda_forge_releases(): + data = conda_forge_releases() + assert '0.4.19.post1' in data diff --git a/napari_update_checker/utils.py b/napari_update_checker/utils.py new file mode 100644 index 0000000..0f72ccb --- /dev/null +++ b/napari_update_checker/utils.py @@ -0,0 +1,30 @@ +import json +from functools import lru_cache +from urllib.request import urlopen + + +@lru_cache +def github_tags(url: str = 'https://api.github.com/repos/napari/napari/tags'): + with urlopen(url) as r: + data = json.load(r) + + versions = [] + for item in data: + version = item.get('name', None) + if version: + if version.startswith('v'): + version = version[1:] + + versions.append(version) + + return list(reversed(versions)) + + +@lru_cache +def conda_forge_releases( + url: str = 'https://api.anaconda.org/package/conda-forge/napari/', +): + with urlopen(url) as r: + data = json.load(r) + versions = data.get('versions', []) + return versions