diff --git a/hikari/api/rest.py b/hikari/api/rest.py index b8ae56c754..ec483e01d4 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -1915,7 +1915,7 @@ async def add_user_to_guild( guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild] The guild to add the user to. This can be a `hikari.guilds.PartialGuild` or the ID of an existing guild. - user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialGuild] + user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser] The user to add to the guild. This can be a `hikari.users.PartialUser` or the ID of an existing user. nick : hikari.undefined.UndefinedOr[builtins.str] @@ -1984,7 +1984,7 @@ async def fetch_user(self, user: snowflakes.SnowflakeishOr[users.PartialUser]) - Parameters ---------- - user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialGuild] + user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser] The user to fetch. This can be a `hikari.users.PartialUser` or the ID of an existing user. diff --git a/hikari/presences.py b/hikari/presences.py index 5090ddcfce..19826366a6 100644 --- a/hikari/presences.py +++ b/hikari/presences.py @@ -86,7 +86,7 @@ class ActivityType(enum.IntEnum): """ COMPETING = 5 - """Shows up as `Competing in `""" + """Shows up as `Competing in `.""" def __str__(self) -> str: return self.name diff --git a/hikari/utilities/ux.py b/hikari/utilities/ux.py index f9b714c4fc..cb95382aca 100644 --- a/hikari/utilities/ux.py +++ b/hikari/utilities/ux.py @@ -24,7 +24,6 @@ __all__: typing.List[str] = ["init_logging", "print_banner", "supports_color", "HikariVersion", "check_for_updates"] -import contextlib import distutils.version import importlib.resources import logging.config @@ -140,6 +139,9 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color: package : typing.Optional[builtins.str] The package to find the `banner.txt` in, or `builtins.None` if no banner should be shown. + + !!! note + The `banner.txt` must be in the root folder of the package. allow_color : builtins.bool If `builtins.False`, no colour is allowed. If `builtins.True`, the output device must be supported for this to return `builtins.True`. @@ -173,9 +175,6 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color: # Python stuff. "python_implementation": platform.python_implementation(), "python_version": platform.python_version(), - "python_build": " ".join(platform.python_build()), - "python_branch": platform.python_branch(), - "python_compiler": platform.python_compiler(), # Platform specific stuff I might remove later. "system_description": " ".join(filtered_system_bits), } @@ -190,30 +189,33 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color: def supports_color(allow_color: bool, force_color: bool) -> bool: - """Return `builtins.True` if the terminal device supports colour output. + """Return `builtins.True` if the terminal device supports color output. Parameters ---------- allow_color : builtins.bool - If `builtins.False`, no colour is allowed. If `builtins.True`, the + If `builtins.False`, no color is allowed. If `builtins.True`, the output device must be supported for this to return `builtins.True`. force_color : builtins.bool If `builtins.True`, return `builtins.True` always, otherwise only - return `builtins.True` if the device supports colour output and the + return `builtins.True` if the device supports color output and the `allow_color` flag is not `builtins.False`. Returns ------- builtins.bool - `builtins.True` if colour is allowed on the output terminal, or + `builtins.True` if color is allowed on the output terminal, or `builtins.False` otherwise. """ + if not allow_color: + return False + # isatty is not always implemented, https://code.djangoproject.com/ticket/6223 is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() if os.getenv("CLICOLOR_FORCE", "0") != "0" or force_color: return True - elif (os.getenv("CLICOLOR", "0") != "0" or allow_color) and is_a_tty: + elif os.getenv("CLICOLOR", "0") != "0" and is_a_tty: return True plat = sys.platform @@ -237,12 +239,33 @@ class HikariVersion(distutils.version.StrictVersion): # Not typed correctly on distutils, so overriding it raises a false positive... version_re: typing.ClassVar[typing.Final[re.Pattern[str]]] = re.compile( # type: ignore[misc] - r"^(\d+)\.(\d+)(\.(\d+))?(\.[a-z]+(\d+))?$", re.I + r"^(\d+)\.(\d+)(\.(\d+))?(\.[a-z]+)?(\d+)?$", re.I ) + # Parse doesnt set the prerelease correctly, so we overwrite it to fix it. + # + # Not typed correctly on distutils, so overriding it raises a false positive... + def parse(self, vstring: str) -> None: # type: ignore[override] + match = self.version_re.match(vstring) + if not match: + raise ValueError(f"invalid version number '{vstring}'") + + (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6) + + self.version = (int(major), int(minor), int(patch) if patch else 0) + + if prerelease: + self.prerelease = (prerelease, int(prerelease_num)) + else: + self.prerelease = None + async def check_for_updates() -> None: """Perform a check for newer versions of the library, logging any found.""" + if about.__git_sha1__.casefold() == "head": + # We are not in a PyPI release, return + return + try: async with aiohttp.request( "GET", "https://pypi.org/pypi/hikari/json", timeout=aiohttp.ClientTimeout(total=1.5), raise_for_status=True @@ -251,24 +274,20 @@ async def check_for_updates() -> None: this_version = HikariVersion(about.__version__) is_dev = this_version.prerelease is not None - is_ambiguous_dev = about.__git_sha1__.casefold() == "head" newer_releases: typing.List[HikariVersion] = [] for release_string, artifacts in data["releases"].items(): if not all(artifact["yanked"] for artifact in artifacts): - with contextlib.suppress(Exception): - v = HikariVersion(release_string) - if v.prerelease is not None and not is_dev: - # Don't encourage the user to upgrade from a stable to a dev release... - continue - - if is_dev and is_ambiguous_dev and v.version == this_version.version: - # i.e. we are a git release of v2.0.0.dev, and we compare to pypi - # 2.0.0.dev67, we cannot determine if we are newer or not... - continue - - if v > this_version: - newer_releases.append(v) + v = HikariVersion(release_string) + if v.prerelease is not None and not is_dev: + # Don't encourage the user to upgrade from a stable to a dev release... + continue + + if v.version == this_version.version: + continue + + if v > this_version: + newer_releases.append(v) if newer_releases: newest = max(newer_releases) _LOGGER.info("A newer version of hikari is available, consider upgrading to %s", newest) diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py index 96a84c40d4..2338b9821e 100644 --- a/tests/hikari/impl/test_shard.py +++ b/tests/hikari/impl/test_shard.py @@ -907,7 +907,8 @@ async def test__identify_when_no_intents(self, client): stack.enter_context(mock.patch.object(aiohttp, "__version__", new="v0.0.1")) stack.enter_context(mock.patch.object(_about, "__version__", new="v1.0.0")) - await client._identify() + with stack: + await client._identify() expected_json = { "op": 2, diff --git a/tests/hikari/utilities/test_ux.py b/tests/hikari/utilities/test_ux.py new file mode 100644 index 0000000000..3d23366c9f --- /dev/null +++ b/tests/hikari/utilities/test_ux.py @@ -0,0 +1,442 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import contextlib +import importlib +import logging +import os +import platform +import string +import sys + +import aiohttp +import colorlog +import mock +import pytest + +from hikari import _about +from hikari.utilities import ux +from tests.hikari import hikari_test_helpers + + +class TestInitLogging: + def test_when_handlers_already_set_up(self): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(logging, "root", handlers=[None])) + logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig")) + logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig")) + colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig")) + + with stack: + ux.init_logging("LOGGING_LEVEL", True, False) + + logging_dict_config.assert_not_called() + logging_basic_config.assert_not_called() + colorlog_basic_config.assert_not_called() + + def test_when_handlers_specify_not_to_set_up(self): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(logging, "root", handlers=[])) + logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig")) + logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig")) + colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig")) + + with stack: + ux.init_logging(None, True, False) + + logging_dict_config.assert_not_called() + logging_basic_config.assert_not_called() + colorlog_basic_config.assert_not_called() + + def test_when_flavour_is_a_dict(self): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(logging, "root", handlers=[])) + logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig")) + logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig")) + colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig")) + + with stack: + ux.init_logging({"hikari": {"level": "INFO"}}, True, False) + + logging_dict_config.assert_called_once_with({"hikari": {"level": "INFO"}}) + logging_basic_config.assert_not_called() + colorlog_basic_config.assert_not_called() + + def test_when_supports_color(self): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(logging, "root", handlers=[])) + logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig")) + logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig")) + colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig")) + supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=True)) + + with stack: + ux.init_logging("LOGGING_LEVEL", True, False) + + logging_dict_config.assert_not_called() + logging_basic_config.assert_not_called() + colorlog_basic_config.assert_called_once_with( + level="LOGGING_LEVEL", + format="%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s %(asctime)23.23s %(bold)s%(name)s: " + "%(thin)s%(message)s%(reset)s", + stream=sys.stderr, + ) + supports_color.assert_called_once_with(True, False) + + def test_when_doesnt_support_color(self): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(logging, "root", handlers=[])) + logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig")) + logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig")) + colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig")) + supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False)) + + with stack: + ux.init_logging("LOGGING_LEVEL", True, False) + + logging_dict_config.assert_not_called() + logging_basic_config.assert_called_once_with( + level="LOGGING_LEVEL", + format="%(levelname)-1.1s %(asctime)23.23s %(name)s: %(message)s", + stream=sys.stderr, + ) + colorlog_basic_config.assert_not_called() + supports_color.assert_called_once_with(True, False) + + +class TestPrintBanner: + def test_when_package_is_none(self): + with mock.patch.object(sys.stdout, "write") as write: + ux.print_banner(None, True, False) + + write.assert_not_called() + + @pytest.fixture() + def mock_args(self): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(platform, "release", return_value="1.0.0")) + stack.enter_context(mock.patch.object(platform, "system", return_value="Potato")) + stack.enter_context(mock.patch.object(platform, "machine", return_value="Machine")) + stack.enter_context(mock.patch.object(platform, "python_implementation", return_value="CPython")) + stack.enter_context(mock.patch.object(platform, "python_version", return_value="4.0.0")) + + stack.enter_context(mock.patch.object(_about, "__version__", new="2.2.2")) + stack.enter_context(mock.patch.object(_about, "__git_sha1__", new="12345678901234567890")) + stack.enter_context(mock.patch.object(_about, "__copyright__", new="© 2020 Nekokatt")) + stack.enter_context(mock.patch.object(_about, "__license__", new="MIT")) + stack.enter_context(mock.patch.object(_about, "__file__", new="~/hikari")) + stack.enter_context(mock.patch.object(_about, "__docs__", new="https://nekokatt.github.io/hikari/docs")) + stack.enter_context(mock.patch.object(_about, "__discord_invite__", new="https://discord.gg/Jx4cNGG")) + stack.enter_context(mock.patch.object(_about, "__url__", new="https://nekokatt.github.io/hikari")) + + with stack: + yield None + + def test_when_supports_color(self, mock_args): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(colorlog, "escape_codes", new={"red": 0, "green": 1, "blue": 2})) + supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=True)) + read_text = stack.enter_context(mock.patch.object(importlib.resources, "read_text")) + template = stack.enter_context(mock.patch.object(string, "Template")) + write = stack.enter_context(mock.patch.object(sys.stdout, "write")) + abspath = stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path")) + dirname = stack.enter_context(mock.patch.object(os.path, "dirname")) + + with stack: + ux.print_banner("hikari", True, False) + + args = { + # Hikari stuff. + "hikari_version": "2.2.2", + "hikari_git_sha1": "12345678", + "hikari_copyright": "© 2020 Nekokatt", + "hikari_license": "MIT", + "hikari_install_location": "some path", + "hikari_documentation_url": "https://nekokatt.github.io/hikari/docs", + "hikari_discord_invite": "https://discord.gg/Jx4cNGG", + "hikari_source_url": "https://nekokatt.github.io/hikari", + "python_implementation": "CPython", + "python_version": "4.0.0", + "system_description": "1.0.0 Potato Machine", + "red": 0, + "green": 1, + "blue": 2, + } + + template.assert_called_once_with(read_text()) + template().safe_substitute.assert_called_once_with(args) + write.assert_called_once_with(template().safe_substitute()) + dirname.assert_called_once_with("~/hikari") + abspath.assert_called_once_with(dirname()) + supports_color.assert_called_once_with(True, False) + + def test_when_doesnt_supports_color(self, mock_args): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(colorlog, "escape_codes", new={"red": 0, "green": 1, "blue": 2})) + supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False)) + read_text = stack.enter_context(mock.patch.object(importlib.resources, "read_text")) + template = stack.enter_context(mock.patch.object(string, "Template")) + write = stack.enter_context(mock.patch.object(sys.stdout, "write")) + abspath = stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path")) + dirname = stack.enter_context(mock.patch.object(os.path, "dirname")) + + with stack: + ux.print_banner("hikari", True, False) + + args = { + # Hikari stuff. + "hikari_version": "2.2.2", + "hikari_git_sha1": "12345678", + "hikari_copyright": "© 2020 Nekokatt", + "hikari_license": "MIT", + "hikari_install_location": "some path", + "hikari_documentation_url": "https://nekokatt.github.io/hikari/docs", + "hikari_discord_invite": "https://discord.gg/Jx4cNGG", + "hikari_source_url": "https://nekokatt.github.io/hikari", + "python_implementation": "CPython", + "python_version": "4.0.0", + "system_description": "1.0.0 Potato Machine", + "red": "", + "green": "", + "blue": "", + } + + template.assert_called_once_with(read_text()) + template().safe_substitute.assert_called_once_with(args) + write.assert_called_once_with(template().safe_substitute()) + dirname.assert_called_once_with("~/hikari") + abspath.assert_called_once_with(dirname()) + supports_color.assert_called_once_with(True, False) + + +class TestSupportsColor: + def test_when_not_allow_color(self): + assert ux.supports_color(False, True) is False + + def test_when_CLICOLOR_FORCE_in_env(self): + with mock.patch.object(os, "getenv", return_value="1") as getenv: + assert ux.supports_color(True, False) is True + + getenv.assert_called_once_with("CLICOLOR_FORCE", "0") + + def test_when_force_color(self): + with mock.patch.object(os, "getenv", return_value="0") as getenv: + assert ux.supports_color(True, True) is True + + getenv.assert_called_once_with("CLICOLOR_FORCE", "0") + + def test_when_CLICOLOR_and_is_a_tty(self): + with mock.patch.object(sys.stdout, "isatty", return_value=True): + with mock.patch.object(os, "getenv", side_effect=["0", "1"]) as getenv: + assert ux.supports_color(True, False) is True + + assert getenv.call_count == 2 + getenv.assert_has_calls([mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0")]) + + def test_when_plat_is_Pocket_PC(self): + stack = contextlib.ExitStack() + getenv = stack.enter_context(mock.patch.object(os, "getenv", return_value="0")) + stack.enter_context(mock.patch.object(sys, "platform", new="Pocket PC")) + + with stack: + assert ux.supports_color(True, False) is False + + assert getenv.call_count == 2 + getenv.assert_has_calls([mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0")]) + + @pytest.mark.parametrize( + ("term_program", "asicon", "isatty", "expected"), + [ + ("mintty", False, True, True), + ("Terminus", False, True, True), + ("some other", True, True, True), + ("some other", False, True, False), + ("some other", False, False, False), + ("mintty", True, False, False), + ("Terminus", True, False, False), + ], + ) + def test_when_plat_is_win32(self, term_program, asicon, isatty, expected): + stack = contextlib.ExitStack() + getenv = stack.enter_context(mock.patch.object(os, "getenv", side_effect=["0", "0", term_program, ""])) + stack.enter_context(mock.patch.object(sys.stdout, "isatty", return_value=isatty)) + stack.enter_context(mock.patch.object(sys, "platform", new="win32")) + stack.enter_context(mock.patch.object(os, "environ", new=["ANSICON"] if asicon else [])) + + with stack: + assert ux.supports_color(True, False) is expected + + assert getenv.call_count == 4 + getenv.assert_has_calls( + [ + mock.call("CLICOLOR_FORCE", "0"), + mock.call("CLICOLOR", "0"), + mock.call("TERM_PROGRAM", None), + mock.call("PYCHARM_HOSTED", ""), + ] + ) + + @pytest.mark.parametrize("isatty", [True, False]) + def test_when_plat_is_not_win32(self, isatty): + stack = contextlib.ExitStack() + getenv = stack.enter_context(mock.patch.object(os, "getenv", side_effect=["0", "0", ""])) + stack.enter_context(mock.patch.object(sys.stdout, "isatty", return_value=isatty)) + stack.enter_context(mock.patch.object(sys, "platform", new="linux")) + + with stack: + assert ux.supports_color(True, False) is isatty + + assert getenv.call_count == 3 + getenv.assert_has_calls( + [mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), mock.call("PYCHARM_HOSTED", "")] + ) + + @pytest.mark.parametrize("isatty", [True, False]) + @pytest.mark.parametrize("plat", ["linux", "win32"]) + def test_when_PYCHARM_HOSTED(self, isatty, plat): + stack = contextlib.ExitStack() + getenv = stack.enter_context(mock.patch.object(os, "getenv", return_value="0")) + stack.enter_context(mock.patch.object(sys.stdout, "isatty", return_value=isatty)) + stack.enter_context(mock.patch.object(sys, "platform", new=plat)) + + with stack: + assert ux.supports_color(True, False) is True + + if plat == "win32": + assert getenv.call_count == 4 + getenv.assert_has_calls( + [ + mock.call("CLICOLOR_FORCE", "0"), + mock.call("CLICOLOR", "0"), + mock.call("TERM_PROGRAM", None), + mock.call("PYCHARM_HOSTED", ""), + ] + ) + else: + assert getenv.call_count == 3 + getenv.assert_has_calls( + [mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), mock.call("PYCHARM_HOSTED", "")] + ) + + +class TestHikariVersionParse: + @pytest.mark.parametrize("v", ["1", "1.0.0dev2"]) + def test_when_version_number_is_invalid(self, v): + with pytest.raises(ValueError, match=rf"invalid version number '{v}'"): + ux.HikariVersion(v) + + def test_when_patch(self): + assert ux.HikariVersion("1.2.3").version == (1, 2, 3) + + def test_when_no_patch(self): + assert ux.HikariVersion("1.2").version == (1, 2, 0) + + def test_when_prerelease(self): + assert ux.HikariVersion("1.2.3.dev99").prerelease == (".dev", 99) + + def test_when_no_prerelease(self): + assert ux.HikariVersion("1.2.3").prerelease is None + + +@pytest.mark.asyncio +class TestCheckForUpdates: + async def test_when_not_official_pypi_release(self): + stack = contextlib.ExitStack() + logger = stack.enter_context(mock.patch.object(ux, "_LOGGER")) + request = stack.enter_context(mock.patch.object(aiohttp, "request")) + stack.enter_context(mock.patch.object(_about, "__git_sha1__", new="HEAD")) + + with stack: + await ux.check_for_updates() + + logger.debug.assert_not_called() + logger.info.assert_not_called() + request.assert_not_called() + + async def test_when_error_fetching(self): + ex = RuntimeError("testing") + stack = contextlib.ExitStack() + logger = stack.enter_context(mock.patch.object(ux, "_LOGGER")) + request = stack.enter_context(mock.patch.object(aiohttp, "request", side_effect=ex)) + stack.enter_context(mock.patch.object(_about, "__git_sha1__", new="1234567890")) + + with stack: + await ux.check_for_updates() + + logger.debug.assert_called_once_with("Failed to fetch hikari version details", exc_info=ex) + request.assert_called_once_with( + "GET", "https://pypi.org/pypi/hikari/json", timeout=aiohttp.ClientTimeout(total=1.5), raise_for_status=True + ) + + async def test_when_no_new_available_releases(self): + data = { + "releases": { + "0.1.0": [{"yanked": False}], + "1.0.0": [{"yanked": False}], + "1.0.0.dev1": [{"yanked": False}], + "1.0.1": [{"yanked": True}], + } + } + _request = hikari_test_helpers.AsyncContextManagerMock() + _request.json = mock.AsyncMock(return_value=data) + stack = contextlib.ExitStack() + logger = stack.enter_context(mock.patch.object(ux, "_LOGGER")) + request = stack.enter_context(mock.patch.object(aiohttp, "request", return_value=_request)) + stack.enter_context(mock.patch.object(_about, "__version__", new="1.0.0")) + stack.enter_context(mock.patch.object(_about, "__git_sha1__", new="1234567890")) + + with stack: + await ux.check_for_updates() + + logger.debug.assert_not_called() + logger.info.assert_not_called() + request.assert_called_once_with( + "GET", "https://pypi.org/pypi/hikari/json", timeout=aiohttp.ClientTimeout(total=1.5), raise_for_status=True + ) + + @pytest.mark.parametrize("v", ["1.0.1", "1.0.1.dev10"]) + async def test_check_for_updates(self, v): + data = { + "releases": { + "0.1.0": [{"yanked": False}], + "1.0.0": [{"yanked": False}], + "1.0.0.dev1": [{"yanked": False}], + v: [{"yanked": False}, {"yanked": True}], + "1.0.2": [{"yanked": True}], + } + } + _request = hikari_test_helpers.AsyncContextManagerMock() + _request.json = mock.AsyncMock(return_value=data) + stack = contextlib.ExitStack() + logger = stack.enter_context(mock.patch.object(ux, "_LOGGER")) + request = stack.enter_context(mock.patch.object(aiohttp, "request", return_value=_request)) + stack.enter_context(mock.patch.object(_about, "__version__", new="1.0.0.dev1")) + stack.enter_context(mock.patch.object(_about, "__git_sha1__", new="1234567890")) + + with stack: + await ux.check_for_updates() + + logger.debug.assert_not_called() + logger.info.assert_called_once_with( + "A newer version of hikari is available, consider upgrading to %s", ux.HikariVersion(v) + ) + request.assert_called_once_with( + "GET", "https://pypi.org/pypi/hikari/json", timeout=aiohttp.ClientTimeout(total=1.5), raise_for_status=True + )