Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions ultraplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@
warnings._warn_ultraplot(f"Invalid user rc file setting: {err}")
_src[_key] = "black" # fill value
from .colors import _cmap_database as colormaps
from .utils import check_for_update

if rc["ultraplot.check_for_latest_version"]:
check_for_update("ultraplot")
5 changes: 5 additions & 0 deletions ultraplot/internals/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,11 @@ def copy(self):
"The format string used to format `pint.Quantity` default unit labels "
"using ``format(units, unitformat)``. See also :rcraw:`autoformat`.",
),
"ultraplot.check_for_latest_version": (
False,
_validate_bool,
"Whether to check for the latest version of UltraPlot on PyPI when importing",
),
}

# Child settings. Changing the parent changes all the children, but
Expand Down
62 changes: 62 additions & 0 deletions ultraplot/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,65 @@ def test_cycle_in_rc_file(tmp_path):
uplt.rc.load(str(rc_file))

assert uplt.rc["cycle"] == "colorblind"


import io
from unittest.mock import patch, MagicMock
from importlib.metadata import PackageNotFoundError
from ultraplot.utils import check_for_update


@patch("builtins.print")
@patch("importlib.metadata.version")
def test_package_not_installed(mock_version, mock_print):
mock_version.side_effect = PackageNotFoundError
check_for_update("fakepkg")
mock_print.assert_not_called()


@patch("builtins.print")
@patch("importlib.metadata.version", return_value="1.0.0")
@patch("urllib.request.urlopen")
def test_network_failure(mock_urlopen, mock_version, mock_print):
mock_urlopen.side_effect = Exception("Network down")
check_for_update("fakepkg")
mock_print.assert_not_called()


@patch("builtins.print")
@patch("importlib.metadata.version", return_value="1.0.0")
@patch("urllib.request.urlopen")
def test_no_update_available(mock_urlopen, mock_version, mock_print):
mock_resp = MagicMock()
mock_resp.__enter__.return_value = io.StringIO('{"info": {"version": "1.0.0"}}')
mock_urlopen.return_value = mock_resp

check_for_update("fakepkg")
mock_print.assert_not_called()


@patch("builtins.print")
@patch("importlib.metadata.version", return_value="1.0.0")
@patch("urllib.request.urlopen")
def test_update_available(mock_urlopen, mock_version, mock_print):
mock_resp = MagicMock()
mock_resp.__enter__.return_value = io.StringIO('{"info": {"version": "1.2.0"}}')
mock_urlopen.return_value = mock_resp

check_for_update("fakepkg")
mock_print.assert_called_once()
msg = mock_print.call_args[0][0]
assert "A newer version of fakepkg is available" in msg
assert "1.0.0 → 1.2.0" in msg


@patch("builtins.print")
@patch("importlib.metadata.version", return_value="1.0.0dev")
@patch("urllib.request.urlopen")
def test_dev_version_skipped(mock_urlopen, mock_version, mock_print):
mock_resp = MagicMock()
mock_resp.__enter__.return_value = io.StringIO('{"info": {"version": "2.0.0"}}')
mock_urlopen.return_value = mock_resp

check_for_update("fakepkg")
mock_print.assert_not_called()
28 changes: 28 additions & 0 deletions ultraplot/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,34 @@ def _check_ranges(
return True


def check_for_update(package_name: str) -> None:
import json
import urllib.request
from importlib.metadata import version, PackageNotFoundError

try:
current_version = version(package_name)
except PackageNotFoundError:
return # package not installed (e.g. during dev)

try:
with urllib.request.urlopen(
f"https://pypi.org/pypi/{package_name}/json", timeout=2
) as resp:
data = json.load(resp)
latest_version = data["info"]["version"]
except Exception:
return # fail silently, e.g. no internet

# Skip local dev versions
if latest_version != current_version and "dev" not in current_version:
print(
f"\033[93m⚠️ A newer version of {package_name} is available: "
f"{current_version} → {latest_version}\n"
f"Run: pip install -U {package_name}\033[0m"
)


# Deprecations
shade, saturate = warnings._rename_objs(
"0.6.0",
Expand Down