Skip to content

Commit

Permalink
Merge pull request #3 from goanpeca/enh/updates
Browse files Browse the repository at this point in the history
Add more typing, separate tests and prepare package for release
  • Loading branch information
goanpeca authored Dec 23, 2024
2 parents 82d8702 + 8515b73 commit 35fdc18
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 96 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The `napari-update-checker` is a plugin that checks for newer versions of napari

![Screenshot of the napari-update-checker interface, showcasing the plugin](https://raw.githubusercontent.com/napari/update-checker/refs/heads/main/images/description.png)

`napari-update-checler` knows how to detect if napari was installed using `conda` or `pip` or if it was installed using the application bundle to provide the correct documentation on how to update napari to the latest version.
`napari-update-checker` knows how to detect if napari was installed using `conda` or `pip` or if it was installed using the application bundle to provide the correct documentation on how to update napari to the latest version.

## Widget

Expand All @@ -31,7 +31,8 @@ software.

If you encounter any problems, please [file an issue] along with a detailed description.

[napari]: https://github.com/napari/napari
[file an issue]: https://github.com/napari/update-checker/issues
[conda-forge]: https://anaconda.org/conda-forge/napari
[PyPI]: https://pypi.org/napari
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
[napari]: https://github.com/napari/napari
57 changes: 52 additions & 5 deletions napari_update_checker/_tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
from napari_update_checker.utils import conda_forge_releases, github_tags
import sys
import tempfile
from pathlib import Path
from urllib.error import HTTPError, URLError

from napari_update_checker.utils import (
conda_forge_releases,
get_latest_version,
github_tags,
is_conda_environment,
is_version_installed,
)


def test_github_tags():
data = github_tags()
assert '0.5.0a1' in data
try:
data = github_tags()
assert data[0] == '0.5.0a1' # Oldest version available
assert len(data) >= 30
except (HTTPError, URLError):
pass


def test_conda_forge_releases():
data = conda_forge_releases()
assert '0.4.19.post1' in data
try:
data = conda_forge_releases()
assert data[0] == '0.2.12' # Oldest version available
assert len(data) >= 35
except (HTTPError, URLError):
pass


def test_get_latest_version():
result = get_latest_version(github=None)
assert result
result = get_latest_version(github=True)
assert result
result = get_latest_version(github=False)
assert result


def test_is_conda_environment():
conda_envs = tempfile.mkdtemp(prefix='envs')
env = Path(conda_envs) / 'env-name'
meta = env / 'conda-meta'
meta.mkdir(parents=True)
assert is_conda_environment(env)
assert not is_conda_environment(meta)


def test_is_version_installed(monkeypatch):
conda_envs = tempfile.mkdtemp(prefix='envs')
env = Path(conda_envs) / 'boom-1.0.0'
monkeypatch.setattr(sys, 'prefix', env)
meta = env / 'conda-meta'
meta.mkdir(parents=True)
assert is_version_installed('1.0.0', pkg_name='boom')
assert not is_version_installed('2.0.0')
135 changes: 47 additions & 88 deletions napari_update_checker/qt_update_checker.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
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.misc import running_as_constructor_app
from napari.utils.notifications import show_warning
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import (
QLabel,
Expand All @@ -24,59 +17,14 @@
)
from superqt import ensure_main_thread

from napari_update_checker.utils import (
get_latest_version,
is_version_installed,
)

ON_BUNDLE = running_as_constructor_app()
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
IGNORE_FILE = "napari-update-ignore.txt"


class UpdateChecker(QWidget):
Expand All @@ -88,6 +36,7 @@ class UpdateChecker(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._current_version = packaging.version.parse(__version__)
self._is_dev = '.dev' in __version__
self._latest_version = None
self._worker = None
self._base_folder = sys.prefix
Expand All @@ -113,7 +62,6 @@ def _check(self):
self._timer.start()

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(
Expand All @@ -138,46 +86,57 @@ def check(self):
self._worker.start()

@ensure_main_thread
def show_version_info(self, latest_version):
def show_version_info(self, latest_version: packaging.version.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

if self._is_dev:
msg = (
f"You use outdated version of napari.<br><br>"
f"You using napari in development mode.<br><br>"
f"Installed version: {my_version}<br>"
f"Current version: {remote_version}<br><br>"
"For more information on how to update <br>"
f'visit the <a href="{url}">online documentation</a><br><br>'
f"Current released version: {remote_version}<br><br>"
)
self.label.setText(msg)
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!<br><br>"
f"Installed version: {my_version}<br><br>"
)
self.label.setText(msg)
if remote_version > my_version and not is_version_installed(
str(remote_version)
):
url = self.URL_BUNDLE if ON_BUNDLE else self.URL_PACKAGE
msg = (
f"You use outdated version of napari.<br><br>"
f"Installed version: {my_version}<br>"
f"Current version: {remote_version}<br><br>"
"For more information on how to update <br>"
f'visit the <a href="{url}">online documentation</a><br><br>'
)
self.label.setText(msg)
if not self._snoozed:
message = QMessageBox(
QMessageBox.Icon.Information,
"New release",
msg,
QMessageBox.StandardButton.Ok
| QMessageBox.StandardButton.Ignore,
)
if message.exec_() == QMessageBox.StandardButton.Ignore: # type: 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!<br><br>"
f"Installed version: {my_version}<br><br>"
)
self.label.setText(msg)


if __name__ == '__main__':
from qtpy.QtWidgets import QApplication

app = QApplication([])
checker = UpdateChecker()
sys.exit(app.exec_())
sys.exit(app.exec_()) # type: ignore
46 changes: 45 additions & 1 deletion napari_update_checker/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import json
import sys
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from pathlib import Path
from typing import Optional
from urllib.error import HTTPError, URLError
from urllib.request import urlopen

import packaging
import packaging.version
from napari.utils.misc import running_as_constructor_app
from napari.utils.notifications import show_warning

ON_BUNDLE = running_as_constructor_app()


@lru_cache
def github_tags(url: str = 'https://api.github.com/repos/napari/napari/tags'):
Expand All @@ -16,7 +28,6 @@ def github_tags(url: str = 'https://api.github.com/repos/napari/napari/tags'):
version = version[1:]

versions.append(version)

return list(reversed(versions))


Expand All @@ -28,3 +39,36 @@ def conda_forge_releases(
data = json.load(r)
versions = data.get('versions', [])
return versions


def get_latest_version(github: Optional[bool] = None):
"""Check latest version between tags and conda forge depending on type of napari install."""
if github is None:
versions_func = conda_forge_releases if ON_BUNDLE else github_tags
else:
versions_func = github_tags if github is True else conda_forge_releases

versions = []
try:
with ThreadPoolExecutor() as executor:
future = executor.submit(versions_func)

versions = future.result()
except (HTTPError, URLError):
show_warning(
'Update checker: There seems to be an issue with network connectivity. '
)
return None

if versions:
yield packaging.version.parse(versions[-1])


def is_conda_environment(path):
return (Path(path) / 'conda-meta').exists()


def is_version_installed(version, pkg_name='napari'):
envs_folder = Path(sys.prefix)
env = envs_folder.parent / f'{pkg_name}-{version}'
return env.exists() and is_conda_environment(env)

0 comments on commit 35fdc18

Please sign in to comment.