diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cf523801e..a527462c27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.8, 3.9, "3.10", 3.11] + python-version: [3.8, 3.9, "3.10", 3.11, 3.12] runs-on: ${{ matrix.os }} @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -49,17 +49,15 @@ jobs: nox -s pytest-all-features -- --cov-append python scripts/ci/normalize_coverage.py - mv .coverage .coverage.${{ matrix.os }}.${{ matrix.python-version }} - name: Upload coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage - path: .coverage.${{ matrix.os }}.${{ matrix.python-version }} + name: .coverage.${{ matrix.os }}.${{ matrix.python-version }} + path: .coverage retention-days: 1 if-no-files-found: error - upload-coverage: needs: [test] runs-on: ubuntu-latest @@ -69,14 +67,31 @@ jobs: uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Download coverage - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage + path: coverages/ + # Not specifying any name will lead to the download of all available artifacts + + # Since artifacts v2, we can no longer upload multiple artifacts + # with the same name and have them re-download as if it were a directory. + # For this reason, we need to download all available artifacts, filter + # out for only coverage files and then place them in the root directory + # with the name of the artifact + - name: Extract individual coverage files + run: | + cd coverages + + for coverage_dir in ./.coverage.*; do + mv "$coverage_dir/.coverage" "../$coverage_dir" + rmdir "$coverage_dir" + done + + cd .. - name: Combine coverage run: | @@ -100,7 +115,7 @@ jobs: uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -157,7 +172,7 @@ jobs: uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -174,7 +189,7 @@ jobs: uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 @@ -188,7 +203,7 @@ jobs: - name: Upload artifacts if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docs path: public/docs diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 19e06cbaa6..d848522208 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,9 +22,9 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/fragments-check.yml b/.github/workflows/fragments-check.yml index f3866844b8..7bf02043ba 100644 --- a/.github/workflows/fragments-check.yml +++ b/.github/workflows/fragments-check.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 299355c853..00ed3d381c 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -32,7 +32,7 @@ jobs: git config --global user.email "90276125+hikari-bot[bot]@users.noreply.github.com" - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92b3306f4f..634436de8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,12 +27,12 @@ jobs: git config --global user.email "90276125+hikari-bot[bot]@users.noreply.github.com" - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Temporarily disable "include administrators" branch protection - uses: benjefferies/branch-protection-bot@1.0.9 + uses: benjefferies/branch-protection-bot@v1.1.2 with: access_token: ${{ steps.generate_token.outputs.token }} enforce_admins: false @@ -46,7 +46,7 @@ jobs: run: bash scripts/ci/release.sh - name: Re-enable "include administrators" branch protection - uses: benjefferies/branch-protection-bot@1.0.9 + uses: benjefferies/branch-protection-bot@v1.1.2 if: always() with: access_token: ${{ steps.generate_token.outputs.token }} diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 100% rename from .readthedocs.yml rename to .readthedocs.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9408455504..abc1e4839f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 2.0.0.dev122 (2023-11-18) + +### Deprecation + +- Deprecate `Permissions.MANAGE_EMOJIS_AND_STICKERS` in favour of `Permissions.MANAGE_GUILD_EXPREASSIONS` ([#1758](https://github.com/hikari-py/hikari/issues/1758)) + +### Features + +- Add Python 3.12 support. ([#1357](https://github.com/hikari-py/hikari/issues/1357)) +- Allow class listeners ([#1661](https://github.com/hikari-py/hikari/issues/1661)) +- Add missing `clear_x` methods to `InteractionMessageBuilder` + - This brings the functionality more in-line with other message edit APIs ([#1740](https://github.com/hikari-py/hikari/issues/1740)) +- Add missing permissions ([#1758](https://github.com/hikari-py/hikari/issues/1758)) + +### Bugfixes + +- Fix optional connection "revoked" field KeyError when fetching connections. ([#1720](https://github.com/hikari-py/hikari/issues/1720)) +- Ensure shard connect and disconnect always get sent in pairs and properly waited for ([#1744](https://github.com/hikari-py/hikari/issues/1744)) +- Improve handing of force exiting a bot (double interrupt) + - Improve exception message + - Reset signal handlers to original ones after no longer capturing signals ([#1745](https://github.com/hikari-py/hikari/issues/1745)) + +--- ## 2.0.0.dev121 (2023-09-10) ### Features diff --git a/README.md b/README.md index 1ad275816e..b0b738d747 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Gateway APIs. Built on good intentions and the hope that it will be extendable and reusable, rather than an obstacle for future development. -Python 3.8, 3.9, 3.10 and 3.11 are currently supported. +Python 3.8, 3.9, 3.10, 3.11 and 3.12 are currently supported. ## Installation @@ -286,11 +286,12 @@ This replaces the default `asyncio` event loop with one that uses `libuv` intern and then amend your script to be something similar to the following example to utilise it in your application: ```py +import asyncio import os if os.name != "nt": import uvloop - uvloop.install() + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # Your code goes here diff --git a/changes/.template.rst b/changes/.template.rst index 1940463947..4a3bcfc249 100644 --- a/changes/.template.rst +++ b/changes/.template.rst @@ -30,3 +30,7 @@ No significant changes. {% endif %} {% endfor %} --- +{# +This comment adds one more newline at the end of the rendered newsfile content. +In this way the there are 2 newlines between the latest release and the previous release content. +#} diff --git a/changes/1661.feature.md b/changes/1661.feature.md deleted file mode 100644 index 793ed5438d..0000000000 --- a/changes/1661.feature.md +++ /dev/null @@ -1 +0,0 @@ -Allow class listeners diff --git a/changes/1720.bugfix.md b/changes/1720.bugfix.md deleted file mode 100644 index 8fd9ed818f..0000000000 --- a/changes/1720.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Fix optional connection "revoked" field KeyError when fetching connections. diff --git a/changes/1740.feature.md b/changes/1740.feature.md deleted file mode 100644 index 5f6ea6400b..0000000000 --- a/changes/1740.feature.md +++ /dev/null @@ -1,2 +0,0 @@ -Add missing `clear_x` methods to `InteractionMessageBuilder` -- This brings the functionality more in-line with other message edit APIs diff --git a/changes/1744.bugfix.md b/changes/1744.bugfix.md deleted file mode 100644 index 3596046f36..0000000000 --- a/changes/1744.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Ensure shard connect and disconnect always get sent in pairs and properly waited for diff --git a/changes/1745.bugfix.md b/changes/1745.bugfix.md deleted file mode 100644 index 36a7ee3dd1..0000000000 --- a/changes/1745.bugfix.md +++ /dev/null @@ -1,3 +0,0 @@ -Improve handing of force exiting a bot (double interrupt) -- Improve exception message -- Reset signal handlers to original ones after no longer capturing signals diff --git a/changes/1762.breaking.md b/changes/1762.breaking.md new file mode 100644 index 0000000000..2e47c33ce2 --- /dev/null +++ b/changes/1762.breaking.md @@ -0,0 +1 @@ +Remove previously deprecated `Permissions.MANAGE_EMOJIS_AND_STICKERS` diff --git a/dev-requirements/build.txt b/dev-requirements/build.txt index 34318c8c4d..c1a30c7ce3 100644 --- a/dev-requirements/build.txt +++ b/dev-requirements/build.txt @@ -1,2 +1,2 @@ setuptools>=65.2.0 -wheel==0.41.3 +wheel==0.42.0 diff --git a/dev-requirements/coverage.txt b/dev-requirements/coverage.txt index d3bf39f7c2..da0b5e9860 100644 --- a/dev-requirements/coverage.txt +++ b/dev-requirements/coverage.txt @@ -1 +1 @@ -coverage[toml]==7.3.2 +coverage[toml]==7.3.3 diff --git a/dev-requirements/formatting.txt b/dev-requirements/formatting.txt index 1176c7b7fd..83663e4cde 100644 --- a/dev-requirements/formatting.txt +++ b/dev-requirements/formatting.txt @@ -1,2 +1,2 @@ -black==23.11.0 -isort==5.12.0 +black==23.12.0 +isort==5.13.2 diff --git a/dev-requirements/mypy.txt b/dev-requirements/mypy.txt index e38deb7ca7..ed32694607 100644 --- a/dev-requirements/mypy.txt +++ b/dev-requirements/mypy.txt @@ -1 +1 @@ -mypy==1.6.1 +mypy==1.7.1 diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt index 6f43e36b1e..9a3760f330 100644 --- a/dev-requirements/pyright.txt +++ b/dev-requirements/pyright.txt @@ -1 +1 @@ -pyright==1.1.336 +pyright==1.1.342 diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt index be98f135da..93e7d3d94c 100644 --- a/dev-requirements/pytest.txt +++ b/dev-requirements/pytest.txt @@ -2,7 +2,7 @@ mock==5.1.0 pytest==7.4.3 -pytest-asyncio==0.21.1 +pytest-asyncio==0.23.2 pytest-cov==4.1.0 pytest-randomly==3.15.0 diff --git a/dev-requirements/sphinx.txt b/dev-requirements/sphinx.txt index 2be214006e..e169bc2d9f 100644 --- a/dev-requirements/sphinx.txt +++ b/dev-requirements/sphinx.txt @@ -11,6 +11,6 @@ numpydoc==1.6.0 myst-parser==2.0.0 # Misc extensions -sphinxext.opengraph==0.9.0 +sphinxext.opengraph==0.9.1 sphinx-copybutton==0.5.2 sphinxcontrib-towncrier==0.4.0a0 diff --git a/hikari/_about.py b/hikari/_about.py index 574f361f20..b05954315a 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -39,5 +39,5 @@ __issue_tracker__: typing.Final[str] = "https://github.com/hikari-py/hikari/issues" __license__: typing.Final[str] = "MIT" __url__: typing.Final[str] = "https://github.com/hikari-py/hikari" -__version__: typing.Final[str] = "2.0.0.dev122" +__version__: typing.Final[str] = "2.0.0.dev123" __git_sha1__: typing.Final[str] = "HEAD" diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py index f944890d46..fb18e4a82e 100644 --- a/hikari/impl/event_manager_base.py +++ b/hikari/impl/event_manager_base.py @@ -543,7 +543,10 @@ def dispatch(self, event: base_events.Event) -> asyncio.Future[typing.Any]: del self._waiters[cls] self._increment_waiter_group_count(cls, -1) - return asyncio.gather(*tasks) if tasks else aio.completed_future() + if tasks: + return asyncio.gather(*tasks) + + return aio.completed_future() def stream( self, diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py index 7bbe4241a9..48f3b9b4ae 100644 --- a/hikari/impl/shard.py +++ b/hikari/impl/shard.py @@ -219,7 +219,8 @@ def _handle_other_message(self, message: aiohttp.WSMessage, /) -> typing.NoRetur close_code = int(message.data) can_reconnect = close_code < 4000 or close_code in _RECONNECTABLE_CLOSE_CODES - raise errors.GatewayServerClosedConnectionError(message.extra, close_code, can_reconnect) + # str(message.extra) is used to cast the possible None to a string + raise errors.GatewayServerClosedConnectionError(str(message.extra), close_code, can_reconnect) if message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: # May be caused by the server shutting us down. diff --git a/hikari/internal/aio.py b/hikari/internal/aio.py index 3b7b75e6d1..e2bb8dc7d6 100644 --- a/hikari/internal/aio.py +++ b/hikari/internal/aio.py @@ -166,7 +166,7 @@ async def all_of(*aws: typing.Awaitable[T_co], timeout: typing.Optional[float] = typing.Sequence[T_co] The results of each awaitable in the order they were invoked in. """ - fs = tuple(map(asyncio.ensure_future, aws)) + fs: typing.Tuple[asyncio.Future[T_co], ...] = tuple(map(asyncio.ensure_future, aws)) gatherer = asyncio.gather(*fs) try: diff --git a/hikari/internal/attrs_extensions.py b/hikari/internal/attrs_extensions.py index 8370eba349..4af6a2a152 100644 --- a/hikari/internal/attrs_extensions.py +++ b/hikari/internal/attrs_extensions.py @@ -79,10 +79,7 @@ def get_fields_definition( init_results: typing.List[typing.Tuple[attrs.Attribute[typing.Any], str]] = [] non_init_results: typing.List[attrs.Attribute[typing.Any]] = [] - # Mypy has a bug where it will always report - # "Argument 1 to "fields" has incompatible type "Type[AttrsInstance]"; expected an attrs class" - # even if the type is correct. - for field in attrs.fields(cls): # type: ignore[misc] + for field in attrs.fields(cls): if field.init: key_word = field.name[1:] if field.name.startswith("_") else field.name init_results.append((field, key_word)) diff --git a/hikari/internal/enums.py b/hikari/internal/enums.py index 9307e8ef2d..bff61dcbc9 100644 --- a/hikari/internal/enums.py +++ b/hikari/internal/enums.py @@ -203,19 +203,21 @@ def __new__( for name, value in namespace.names_to_values.items(): member = new_namespace.get(name) - if not isinstance(member, _DeprecatedAlias): - # Patching the member init call is around 100ns faster per call than - # using the default type.__call__ which would make us do the lookup - # in cls.__new__. Reason for this is that python will also always - # invoke cls.__init__ if we do this, so we end up with two function - # calls. - member = cls.__new__(cls, value) - member._name_ = name - member._value_ = value - setattr(cls, name, member) + if isinstance(member, _DeprecatedAlias): + continue + + # Patching the member init call is around 100ns faster per call than + # using the default type.__call__ which would make us do the lookup + # in cls.__new__. Reason for this is that python will also always + # invoke cls.__init__ if we do this, so we end up with two function + # calls. + member = cls.__new__(cls, value) + member._name_ = name + member._value_ = value + setattr(cls, name, member) name_to_member[name] = member - value_to_member.setdefault(value, member) + value_to_member[value] = member member_names.append(name) return cls @@ -471,24 +473,26 @@ def __new__( for name, value in namespace.names_to_values.items(): member = new_namespace.get(name) - if not isinstance(member, _DeprecatedAlias): - # Patching the member init call is around 100ns faster per call than - # using the default type.__call__ which would make us do the lookup - # in cls.__new__. Reason for this is that python will also always - # invoke cls.__init__ if we do this, so we end up with two function - # calls. - member = cls.__new__(cls, value) - member._name_ = name - member._value_ = value - setattr(cls, name, member) - - if not (value & value - 1): - powers_of_2_map[value] = member + if isinstance(member, _DeprecatedAlias): + continue + + # Patching the member init call is around 100ns faster per call than + # using the default type.__call__ which would make us do the lookup + # in cls.__new__. Reason for this is that python will also always + # invoke cls.__init__ if we do this, so we end up with two function + # calls. + member = cls.__new__(cls, value) + member._name_ = name + member._value_ = value + setattr(cls, name, member) name_to_member[name] = member - value_to_member.setdefault(value, member) + value_to_member[value] = member member_names.append(name) + if not (value & value - 1): + powers_of_2_map[value] = member + all_bits = functools.reduce(operator.or_, value_to_member.keys()) all_bits_member = cls.__new__(cls, all_bits) all_bits_member._name_ = None diff --git a/hikari/internal/fast_protocol.py b/hikari/internal/fast_protocol.py index a313578039..2e9f3b6f64 100644 --- a/hikari/internal/fast_protocol.py +++ b/hikari/internal/fast_protocol.py @@ -32,7 +32,7 @@ from typing_extensions import Self _Protocol: FastProtocolChecking = NotImplemented -_IGNORED_ATTRS = typing.EXCLUDED_ATTRIBUTES + ["__qualname__", "__slots__"] +_IGNORED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | {"__qualname__", "__slots__"} def _check_if_ignored(name: str) -> bool: diff --git a/hikari/permissions.py b/hikari/permissions.py index c6d1aa46c4..f709c49be8 100644 --- a/hikari/permissions.py +++ b/hikari/permissions.py @@ -227,8 +227,8 @@ class Permissions(enums.Flag): (or their owner's account in the case of bot users) and the guild owner. """ - MANAGE_EMOJIS_AND_STICKERS = 1 << 30 - """Allows management and editing of emojis and stickers. + MANAGE_GUILD_EXPRESSIONS = 1 << 30 + """Allows management and editing emojis, stickers and soundboard sounds. .. note:: In guilds with server-wide 2FA enabled this permission can only be used @@ -249,7 +249,7 @@ class Permissions(enums.Flag): """ MANAGE_EVENTS = 1 << 33 - """Allows for creating, editing, and deleting scheduled events """ + """Allows for management and editing scheduled events""" MANAGE_THREADS = 1 << 34 """Allows for deleting and archiving threads, and viewing all private threads. @@ -284,6 +284,21 @@ class Permissions(enums.Flag): USE_SOUNDBOARD = 1 << 42 """Allows the use of soundboard in a voice chat.""" + CREATE_GUILD_EXPRESSIONS = 1 << 43 + """Allows to create guild emojis, stickers and soundboard sounds. + + Additionally, it allows to edit and manage those created by the user. + """ + + CREATE_EVENTS = 1 << 44 + """Allows to create scheduled events. + + Additionally, it allows to edit and manage those created by the user. + """ + + USE_EXTERNAL_SOUNDS = 1 << 45 + """Allows the use of soundboard sounds from external servers.""" + SEND_VOICE_MESSAGES = 1 << 46 """Allows sending voice messages.""" diff --git a/requirements.txt b/requirements.txt index 1526161d40..31e1062236 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiohttp~=3.8 +aiohttp~=3.9 attrs~=23.1 -colorlog~=6.7 +colorlog~=6.8 multidict~=6.0 diff --git a/setup.py b/setup.py index 6ec507f638..e70e11c81a 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ def parse_requirements_file(path): maintainer_email=metadata.email, license=metadata.license, url=metadata.url, - python_requires=">=3.8.0,<3.12", + python_requires=">=3.8.0,<3.13", packages=setuptools.find_namespace_packages(include=["hikari*"]), entry_points={"console_scripts": ["hikari = hikari.cli:main"]}, install_requires=parse_requirements_file("requirements.txt"), @@ -95,6 +95,7 @@ def parse_requirements_file(path): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Communications :: Chat", "Topic :: Internet :: WWW/HTTP", diff --git a/speedup-requirements.txt b/speedup-requirements.txt index 1084ff458c..dee9db84b2 100644 --- a/speedup-requirements.txt +++ b/speedup-requirements.txt @@ -1,3 +1,3 @@ -aiohttp[speedups]~=3.8 +aiohttp[speedups]~=3.9 ciso8601~=2.3 orjson~=3.9 diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py index ebba1c2ec6..7e16aee6f3 100644 --- a/tests/hikari/impl/test_event_manager_base.py +++ b/tests/hikari/impl/test_event_manager_base.py @@ -544,10 +544,11 @@ async def test_consume_raw_event_skips_raw_dispatch_when_not_enabled(self, event event_manager._enabled_for_event.assert_called_once_with(shard_events.ShardPayloadEvent) @pytest.mark.asyncio() - async def test_handle_dispatch_invokes_callback(self, event_manager, event_loop): + async def test_handle_dispatch_invokes_callback(self, event_manager): event_manager._enabled_for_consumer = mock.Mock(return_value=True) consumer = mock.AsyncMock() error_handler = mock.MagicMock() + event_loop = asyncio.get_running_loop() event_loop.set_exception_handler(error_handler) shard = object() pl = {"foo": "bar"} @@ -558,10 +559,11 @@ async def test_handle_dispatch_invokes_callback(self, event_manager, event_loop) error_handler.assert_not_called() @pytest.mark.asyncio() - async def test_handle_dispatch_ignores_cancelled_errors(self, event_manager, event_loop): + async def test_handle_dispatch_ignores_cancelled_errors(self, event_manager): event_manager._enabled_for_consumer = mock.Mock(return_value=True) consumer = mock.AsyncMock(side_effect=asyncio.CancelledError) error_handler = mock.MagicMock() + event_loop = asyncio.get_running_loop() event_loop.set_exception_handler(error_handler) shard = object() pl = {"lorem": "ipsum"} @@ -571,27 +573,33 @@ async def test_handle_dispatch_ignores_cancelled_errors(self, event_manager, eve error_handler.assert_not_called() @pytest.mark.asyncio() - async def test_handle_dispatch_handles_exceptions(self, event_manager, event_loop): + async def test_handle_dispatch_handles_exceptions(self, event_manager): + mock_task = mock.Mock() + # On Python 3.12+ Asyncio uses this to get the task's context if set to call the + # error handler in. We want to avoid for this test for simplicity. + mock_task.get_context.return_value = None event_manager._enabled_for_consumer = mock.Mock(return_value=True) exc = Exception("aaaa!") consumer = mock.Mock(callback=mock.AsyncMock(side_effect=exc)) error_handler = mock.MagicMock() + event_loop = asyncio.get_running_loop() event_loop.set_exception_handler(error_handler) shard = object() pl = {"i like": "cats"} - with mock.patch.object(asyncio, "current_task") as current_task: + with mock.patch.object(asyncio, "current_task", return_value=mock_task): await event_manager._handle_dispatch(consumer, shard, pl) error_handler.assert_called_once_with( event_loop, - {"exception": exc, "message": "Exception occurred in raw event dispatch conduit", "task": current_task()}, + {"exception": exc, "message": "Exception occurred in raw event dispatch conduit", "task": mock_task}, ) @pytest.mark.asyncio() - async def test_handle_dispatch_invokes_when_consumer_not_enabled(self, event_manager, event_loop): + async def test_handle_dispatch_invokes_when_consumer_not_enabled(self, event_manager): consumer = mock.Mock(callback=mock.AsyncMock(__name__="ok"), is_enabled=False) error_handler = mock.MagicMock() + event_loop = asyncio.get_running_loop() event_loop.set_exception_handler(error_handler) shard = object() pl = {"foo": "bar"} diff --git a/tests/hikari/impl/test_rate_limits.py b/tests/hikari/impl/test_rate_limits.py index c36a5120e9..2855e65d2c 100644 --- a/tests/hikari/impl/test_rate_limits.py +++ b/tests/hikari/impl/test_rate_limits.py @@ -65,14 +65,16 @@ def test_is_empty(self, queue, is_empty, mock_burst_limiter): mock_burst_limiter.queue = queue assert mock_burst_limiter.is_empty is is_empty - def test_close_removes_all_futures_from_queue(self, event_loop, mock_burst_limiter): + def test_close_removes_all_futures_from_queue(self, mock_burst_limiter): + event_loop = asyncio.get_event_loop() mock_burst_limiter.throttle_task = None futures = [event_loop.create_future() for _ in range(10)] mock_burst_limiter.queue = list(futures) mock_burst_limiter.close() assert len(mock_burst_limiter.queue) == 0 - def test_close_cancels_all_futures_pending_when_futures_pending(self, event_loop, mock_burst_limiter): + def test_close_cancels_all_futures_pending_when_futures_pending(self, mock_burst_limiter): + event_loop = asyncio.get_event_loop() mock_burst_limiter.throttle_task = None futures = [event_loop.create_future() for _ in range(10)] mock_burst_limiter.queue = list(futures) @@ -86,7 +88,8 @@ def test_close_is_silent_when_no_futures_pending(self, mock_burst_limiter): mock_burst_limiter.close() assert True, "passed successfully" - def test_close_cancels_throttle_task_if_running(self, event_loop, mock_burst_limiter): + def test_close_cancels_throttle_task_if_running(self, mock_burst_limiter): + event_loop = asyncio.get_event_loop() task = event_loop.create_future() mock_burst_limiter.throttle_task = task mock_burst_limiter.close() @@ -101,7 +104,9 @@ def test_close_when_closed(self, mock_burst_limiter): class TestManualRateLimiter: @pytest.mark.asyncio() - async def test_acquire_returns_completed_future_if_throttle_task_is_None(self, event_loop): + async def test_acquire_returns_completed_future_if_throttle_task_is_None(self): + event_loop = asyncio.get_event_loop() + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = None future = MockFuture() @@ -111,7 +116,9 @@ async def test_acquire_returns_completed_future_if_throttle_task_is_None(self, e future.set_result.assert_called_once_with(None) @pytest.mark.asyncio() - async def test_acquire_returns_incomplete_future_if_throttle_task_is_not_None(self, event_loop): + async def test_acquire_returns_incomplete_future_if_throttle_task_is_not_None(self): + event_loop = asyncio.get_event_loop() + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = event_loop.create_future() future = MockFuture() @@ -121,7 +128,9 @@ async def test_acquire_returns_incomplete_future_if_throttle_task_is_not_None(se future.set_result.assert_not_called() @pytest.mark.asyncio() - async def test_acquire_places_future_on_queue_if_throttle_task_is_not_None(self, event_loop): + async def test_acquire_places_future_on_queue_if_throttle_task_is_not_None(self): + event_loop = asyncio.get_event_loop() + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = event_loop.create_future() future = MockFuture() @@ -153,7 +162,9 @@ async def test_throttle_schedules_throttle(self): limiter.unlock_later.assert_called_once_with(0) @pytest.mark.asyncio() - async def test_throttle_chews_queue_completing_futures(self, event_loop): + async def test_throttle_chews_queue_completing_futures(self): + event_loop = asyncio.get_event_loop() + with rate_limits.ManualRateLimiter() as limiter: futures = [event_loop.create_future() for _ in range(10)] limiter.queue = list(futures) @@ -162,7 +173,8 @@ async def test_throttle_chews_queue_completing_futures(self, event_loop): assert future.done(), f"future {i} was not done" @pytest.mark.asyncio() - async def test_throttle_sleeps_before_popping_queue(self, event_loop): + async def test_throttle_sleeps_before_popping_queue(self): + event_loop = asyncio.get_event_loop() # GIVEN slept_at = float("nan") popped_at = [] @@ -188,7 +200,9 @@ def pop(self, _=-1): assert slept_at < pop_time, f"future {i} popped before initial sleep" @pytest.mark.asyncio() - async def test_throttle_clears_throttle_task(self, event_loop): + async def test_throttle_clears_throttle_task(self): + event_loop = asyncio.get_running_loop() + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = event_loop.create_future() await limiter.unlock_later(0) @@ -206,7 +220,9 @@ def ratelimiter(self): inst.close() @pytest.mark.asyncio() - async def test_drip_if_not_throttled_and_not_ratelimited(self, ratelimiter, event_loop): + async def test_drip_if_not_throttled_and_not_ratelimited(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = None ratelimiter.is_rate_limited = mock.Mock(return_value=False) @@ -218,7 +234,9 @@ async def test_drip_if_not_throttled_and_not_ratelimited(self, ratelimiter, even event_loop.create_future.assert_not_called() @pytest.mark.asyncio() - async def test_no_drip_if_throttle_task_is_not_None(self, ratelimiter, event_loop): + async def test_no_drip_if_throttle_task_is_not_None(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = asyncio.get_running_loop().create_future() ratelimiter.is_rate_limited = mock.Mock(return_value=False) @@ -230,7 +248,9 @@ async def test_no_drip_if_throttle_task_is_not_None(self, ratelimiter, event_loo ratelimiter.drip.assert_not_called() @pytest.mark.asyncio() - async def test_no_drip_if_rate_limited(self, ratelimiter, event_loop): + async def test_no_drip_if_rate_limited(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = False ratelimiter.is_rate_limited = mock.Mock(return_value=True) @@ -242,7 +262,9 @@ async def test_no_drip_if_rate_limited(self, ratelimiter, event_loop): ratelimiter.drip.assert_not_called() @pytest.mark.asyncio() - async def test_task_scheduled_if_rate_limited_and_throttle_task_is_None(self, ratelimiter, event_loop): + async def test_task_scheduled_if_rate_limited_and_throttle_task_is_None(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = None ratelimiter.throttle = mock.AsyncMock() @@ -256,7 +278,9 @@ async def test_task_scheduled_if_rate_limited_and_throttle_task_is_None(self, ra ratelimiter.throttle.assert_called() @pytest.mark.asyncio() - async def test_task_not_scheduled_if_rate_limited_and_throttle_task_not_None(self, ratelimiter, event_loop): + async def test_task_not_scheduled_if_rate_limited_and_throttle_task_not_None(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = event_loop.create_future() old_task = ratelimiter.throttle_task @@ -268,7 +292,9 @@ async def test_task_not_scheduled_if_rate_limited_and_throttle_task_not_None(sel assert old_task is ratelimiter.throttle_task, "task was rescheduled, that shouldn't happen :(" @pytest.mark.asyncio() - async def test_future_is_added_to_queue_if_throttle_task_is_not_None(self, ratelimiter, event_loop): + async def test_future_is_added_to_queue_if_throttle_task_is_not_None(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = asyncio.get_running_loop().create_future() ratelimiter.is_rate_limited = mock.Mock(return_value=False) @@ -281,7 +307,9 @@ async def test_future_is_added_to_queue_if_throttle_task_is_not_None(self, ratel assert ratelimiter.queue[-1:] == [future] @pytest.mark.asyncio() - async def test_future_is_added_to_queue_if_rate_limited(self, ratelimiter, event_loop): + async def test_future_is_added_to_queue_if_rate_limited(self, ratelimiter): + event_loop = asyncio.get_running_loop() + ratelimiter.drip = mock.Mock() ratelimiter.throttle_task = None ratelimiter.is_rate_limited = mock.Mock(return_value=True) @@ -296,7 +324,9 @@ async def test_future_is_added_to_queue_if_rate_limited(self, ratelimiter, event ratelimiter.throttle_task.cancel() @pytest.mark.asyncio() - async def test_throttle_consumes_queue(self, event_loop): + async def test_throttle_consumes_queue(self): + event_loop = asyncio.get_running_loop() + with mock.patch.object(asyncio, "sleep"): with rate_limits.WindowedBurstRateLimiter(__name__, 0.001, 1) as rl: rl.queue = [event_loop.create_future() for _ in range(15)] @@ -308,7 +338,9 @@ async def test_throttle_consumes_queue(self, event_loop): assert future.done(), f"future {i} was incomplete!" @pytest.mark.asyncio() - async def test_throttle_when_limited_sleeps_then_bursts_repeatedly(self, event_loop): + async def test_throttle_when_limited_sleeps_then_bursts_repeatedly(self): + event_loop = asyncio.get_running_loop() + window = 5 loop_count = 0 futures = [event_loop.create_future() for _ in range(20)] @@ -351,7 +383,9 @@ def mock_get_time_until_reset(_self, _): assert future.done(), f"future {i} was incomplete!" @pytest.mark.asyncio() - async def test_throttle_resets_throttle_task(self, event_loop): + async def test_throttle_resets_throttle_task(self): + event_loop = asyncio.get_running_loop() + with rate_limits.WindowedBurstRateLimiter(__name__, 0.001, 1) as rl: rl.queue = [event_loop.create_future() for _ in range(15)] rl.throttle_task = None diff --git a/tests/hikari/internal/test_aio.py b/tests/hikari/internal/test_aio.py index d50317470c..2b07d5f661 100644 --- a/tests/hikari/internal/test_aio.py +++ b/tests/hikari/internal/test_aio.py @@ -201,7 +201,9 @@ async def __anext__(self): @pytest.mark.asyncio() class TestFirstCompleted: @hikari_test_helpers.timeout() - async def test_first_future_completes(self, event_loop): + async def test_first_future_completes(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -213,7 +215,9 @@ async def test_first_future_completes(self, event_loop): assert f3.cancelled() @hikari_test_helpers.timeout() - async def test_second_future_completes(self, event_loop): + async def test_second_future_completes(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -225,7 +229,9 @@ async def test_second_future_completes(self, event_loop): assert f3.cancelled() @hikari_test_helpers.timeout() - async def test_timeout_propagates(self, event_loop): + async def test_timeout_propagates(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -238,7 +244,9 @@ async def test_timeout_propagates(self, event_loop): assert f3.cancelled() @hikari_test_helpers.timeout() - async def test_cancelled_propagates(self, event_loop): + async def test_cancelled_propagates(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -254,7 +262,9 @@ async def test_cancelled_propagates(self, event_loop): assert f3.cancelled() @hikari_test_helpers.timeout() - async def test_single_cancelled_propagates(self, event_loop): + async def test_single_cancelled_propagates(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -270,7 +280,9 @@ async def test_single_cancelled_propagates(self, event_loop): # If the CI runners are slow, this may be flaky. @hikari_test_helpers.retry(3) @hikari_test_helpers.timeout() - async def test_result_propagates(self, event_loop): + async def test_result_propagates(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -286,7 +298,9 @@ async def test_result_propagates(self, event_loop): # If the CI runners are slow, this may be flaky. @hikari_test_helpers.retry(3) @hikari_test_helpers.timeout() - async def test_exception_propagates(self, event_loop): + async def test_exception_propagates(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -307,7 +321,9 @@ class TestAllOf: # If the CI runners are slow, this may be flaky. @hikari_test_helpers.retry(3) @hikari_test_helpers.timeout() - async def test_waits_for_all(self, event_loop): + async def test_waits_for_all(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -341,7 +357,9 @@ async def quickly_run_task(task): # If the CI runners are slow, this may be flaky. @hikari_test_helpers.retry(3) @hikari_test_helpers.timeout() - async def test_cancels_all_if_one_errors(self, event_loop): + async def test_cancels_all_if_one_errors(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -360,7 +378,9 @@ async def test_cancels_all_if_one_errors(self, event_loop): # If the CI runners are slow, this may be flaky. @hikari_test_helpers.retry(3) @hikari_test_helpers.timeout() - async def test_cancels_all_if_timeout(self, event_loop): + async def test_cancels_all_if_timeout(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() @@ -374,7 +394,9 @@ async def test_cancels_all_if_timeout(self, event_loop): # If the CI runners are slow, this may be flaky. @hikari_test_helpers.retry(3) @hikari_test_helpers.timeout() - async def test_cancels_all_if_cancelled(self, event_loop): + async def test_cancels_all_if_cancelled(self): + event_loop = asyncio.get_running_loop() + f1 = event_loop.create_future() f2 = event_loop.create_future() f3 = event_loop.create_future() diff --git a/tests/hikari/test_permissions.py b/tests/hikari/test_permissions.py index c119ddd546..3512f805a4 100644 --- a/tests/hikari/test_permissions.py +++ b/tests/hikari/test_permissions.py @@ -27,4 +27,4 @@ def test_all_permissions(self): all_perms = permissions.Permissions.all_permissions() assert isinstance(all_perms, permissions.Permissions) - assert all_perms == 79164837199871 + assert all_perms == 140737488355327