From 451bd3ebc862056c20fdd9ef6189f8e95de81c0a Mon Sep 17 00:00:00 2001 From: Saugat Pachhai Date: Mon, 16 Aug 2021 21:16:44 +0545 Subject: [PATCH] Change ui messaging for the updater (#6437) --- dvc/command/version.py | 13 +---- dvc/updater.py | 101 +++++++++++++++++++++---------------- tests/unit/test_updater.py | 66 +++++++++++++++++++++--- 3 files changed, 118 insertions(+), 62 deletions(-) diff --git a/dvc/command/version.py b/dvc/command/version.py index 5fd70b786..ca8c72eaf 100644 --- a/dvc/command/version.py +++ b/dvc/command/version.py @@ -10,21 +10,12 @@ logger = logging.getLogger(__name__) class CmdVersion(CmdBaseNoRepo): def run(self): from dvc.info import get_dvc_info - from dvc.repo import NotDvcRepoError, Repo + from dvc.updater import notify_updates dvc_info = get_dvc_info() ui.write(dvc_info, force=True) - try: - with Repo() as repo: - from dvc.updater import Updater - - hardlink_lock = repo.config["core"].get("hardlink_lock", False) - updater = Updater(repo.tmp_dir, hardlink_lock=hardlink_lock) - updater.check() - except NotDvcRepoError: - pass - + notify_updates() return 0 diff --git a/dvc/updater.py b/dvc/updater.py index 92c69d2ac..ea03dde9c 100644 --- a/dvc/updater.py +++ b/dvc/updater.py @@ -2,17 +2,20 @@ import logging import os import sys import time +from typing import TYPE_CHECKING, Optional -import colorama from packaging import version from dvc import __version__ from dvc.utils.pkg import PKG +if TYPE_CHECKING: + from rich.text import Text + logger = logging.getLogger(__name__) -class Updater: # pragma: no cover +class Updater: URL = "https://updater.dvc.org" UPDATER_FILE = "updater" TIMEOUT = 24 * 60 * 60 # every day @@ -105,57 +108,58 @@ class Updater: # pragma: no cover with open(self.updater_file, "w+") as fobj: json.dump(info, fobj) - def _notify(self, latest): + def _notify(self, latest: str, pkg: Optional[str] = PKG) -> None: + from dvc.ui import ui + if not sys.stdout.isatty(): return - message = ( - "Update available {red}{current}{reset} -> {green}{latest}{reset}" - + "\n" - + self._get_update_instructions() - ).format( - red=colorama.Fore.RED, - reset=colorama.Fore.RESET, - green=colorama.Fore.GREEN, - yellow=colorama.Fore.YELLOW, - blue=colorama.Fore.BLUE, - current=self.current, - latest=latest, + message = self._get_message(latest, pkg=pkg) + return ui.error_write(message, styled=True) + + def _get_message( + self, + latest: str, + current: str = None, + color: str = "yellow", + pkg: Optional[str] = None, + ) -> "Text": + from rich.text import Text + + current = current or self.current + update_message = Text.from_markup( + f"You are using dvc version [bold]{current}[/]; " + f"however, version [bold]{latest}[/] is available." + ) + instruction = Text.from_markup(self._get_update_instructions(pkg=pkg)) + return Text.assemble( + "\n", update_message, "\n", instruction, style=color ) - from dvc.utils import boxify + def _get_update_instructions(self, pkg: Optional[str] = None) -> str: + if pkg in ("osxpkg", "exe", "binary"): + return ( + "To upgrade, uninstall dvc and reinstall from " + "[blue]https://dvc.org[/]." + ) - logger.info(boxify(message, border_color="yellow")) - - def _get_update_instructions(self): instructions = { - "pip": "Run `{yellow}pip{reset} install dvc " - "{blue}--upgrade{reset}`", - "rpm": "Run `{yellow}yum{reset} update dvc`", - "brew": "Run `{yellow}brew{reset} upgrade dvc`", - "deb": ( - "Run `{yellow}apt-get{reset} install" - " {blue}--only-upgrade{reset} dvc`" - ), - "binary": ( - "To upgrade follow these steps:\n" - "1. Uninstall dvc binary\n" - "2. Go to {blue}https://dvc.org{reset}\n" - "3. Download and install new binary" - ), - "conda": "Run `{yellow}conda{reset} update dvc`", - "choco": "Run `{yellow}choco{reset} upgrade dvc`", - None: ( - "Find the latest release at\n" - "{blue}https://github.com/iterative/dvc/releases/latest{reset}" - ), + "pip": "pip install --upgrade dvc", + "rpm": "yum update dvc", + "brew": "brew upgrade dvc", + "deb": "apt-get install --only-upgrade dvc", + "conda": "conda update dvc", + "choco": "choco upgrade dvc", } - package_manager = PKG - if package_manager in ("osxpkg", "exe"): - package_manager = "binary" + if pkg not in instructions: + return ( + "Find the latest release at " + "[blue]https://github.com/iterative/dvc/releases/latest[/]." + ) - return instructions[package_manager] + instruction = instructions[pkg] + return f"To upgrade, run '{instruction}'." def is_enabled(self): from dvc.config import Config, to_bool @@ -167,3 +171,14 @@ class Updater: # pragma: no cover "Check for update is {}abled.".format("en" if enabled else "dis") ) return enabled + + +def notify_updates(): + from contextlib import suppress + + from dvc.repo import NotDvcRepoError, Repo + + with suppress(NotDvcRepoError), Repo() as repo: + hardlink_lock = repo.config["core"].get("hardlink_lock", False) + updater = Updater(repo.tmp_dir, hardlink_lock=hardlink_lock) + updater.check() diff --git a/tests/unit/test_updater.py b/tests/unit/test_updater.py index 854f408a4..5ea30bd61 100644 --- a/tests/unit/test_updater.py +++ b/tests/unit/test_updater.py @@ -8,6 +8,7 @@ import pytest from dvc import __version__ from dvc.updater import Updater +from tests.func.parsing.test_errors import escape_ansi @pytest.fixture @@ -85,19 +86,26 @@ def test_check_update_respect_config(mock_check, result, updater, mocker): ], ids=["uptodate", "behind", "ahead"], ) -def test_check_updates(mock_tty, updater, caplog, current, latest, notify): +def test_check_updates(mocker, capsys, updater, current, latest, notify): + mocker.patch("sys.stdout.isatty", return_value=True) + updater.current = current with open(updater.updater_file, "w+") as f: json.dump({"version": latest}, f) - caplog.clear() - with caplog.at_level(logging.INFO, logger="dvc.updater"): - updater.check() + updater.check() + out, err = capsys.readouterr() + expected_message = ( + ( + f"You are using dvc version {current}; " + f"however, version {latest} is available.\n" + ) + if notify + else "" + ) - if notify: - assert f"Update available {current} -> {latest}" in caplog.text - else: - assert not caplog.text + assert expected_message in escape_ansi(err) + assert not out def test_check_refetches_each_day(mock_tty, updater, caplog, mocker): @@ -138,3 +146,45 @@ def test_check(mock_check, updater): updater.check() assert mock_check.call_count == 3 + + +@pytest.mark.parametrize( + "pkg, instruction", + [ + ("pip", "To upgrade, run 'pip install --upgrade dvc'."), + ("rpm", "To upgrade, run 'yum update dvc'."), + ("brew", "To upgrade, run 'brew upgrade dvc'."), + ("deb", "To upgrade, run 'apt-get install --only-upgrade dvc'."), + ("conda", "To upgrade, run 'conda update dvc'."), + ("choco", "To upgrade, run 'choco upgrade dvc'."), + ( + "osxpkg", + "To upgrade, uninstall dvc and reinstall from https://dvc.org.", + ), + ( + "exe", + "To upgrade, uninstall dvc and reinstall from https://dvc.org.", + ), + ( + "binary", + "To upgrade, uninstall dvc and reinstall from https://dvc.org.", + ), + ( + None, + "Find the latest release at " + "https://github.com/iterative/dvc/releases/latest.", + ), + ( + "unknown", + "Find the latest release at " + "https://github.com/iterative/dvc/releases/latest.", + ), + ], +) +def test_notify_message(updater, pkg, instruction): + update_message = ( + "You are using dvc version 0.0.2; however, version 0.0.3 is available." + ) + + message = updater._get_message("0.0.3", current="0.0.2", pkg=pkg) + assert message.plain.splitlines() == ["", update_message, instruction]