Repository: hikari-py/hikari
Pull request #1419 (2022-12-24 10:27:22)

Improve error raised when attempting to use an asynchronous iterator synchronously.


Patch:
diff --git a/changes/1425.bugfix.md b/changes/1425.bugfix.md
new file mode 100644
index 00000000..69db2ca1
--- /dev/null
+++ b/changes/1425.bugfix.md
@@ -0,0 +1,1 @@
+Suppress incorrect deprecation warning regarding event loops.
diff --git a/changes/1426.breaking.md b/changes/1426.breaking.md
new file mode 100644
index 00000000..20e61051
--- /dev/null
+++ b/changes/1426.breaking.md
@@ -0,0 +1,1 @@
+Remove long deprecated `async with` support for `EventStream`.
diff --git a/changes/1434.bugfix.md b/changes/1434.bugfix.md
new file mode 100644
index 00000000..5dc7bcb6
--- /dev/null
+++ b/changes/1434.bugfix.md
@@ -0,0 +1,2 @@
+Properly close unclosed file descriptor when reading banner.
+- This only affects versions of Python >= 3.9.
diff --git a/changes/1435.bugfix.md b/changes/1435.bugfix.md
new file mode 100644
index 00000000..82c45ca6
--- /dev/null
+++ b/changes/1435.bugfix.md
@@ -0,0 +1,1 @@
+Start GC of bucket manager when creating a rest client.
diff --git a/changes/idk.feature.md b/changes/idk.feature.md
new file mode 100644
index 00000000..946bfcd2
--- /dev/null
+++ b/changes/idk.feature.md
@@ -0,0 +1,1 @@
+Improve error raised when attempting to use an asynchronous iterator synchronously.
diff --git a/dev-requirements/coverage.txt b/dev-requirements/coverage.txt
index 6c1891d8..068b9d27 100644
--- a/dev-requirements/coverage.txt
+++ b/dev-requirements/coverage.txt
@@ -1,1 +1,1 @@
-coverage[toml]==7.0.0
+coverage[toml]==7.0.1
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index 7d6ab156..a62672f6 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.285
+pyright==1.1.286
diff --git a/dev-requirements/slotscheck.txt b/dev-requirements/slotscheck.txt
index 89c1338f..9fb1f6e7 100644
--- a/dev-requirements/slotscheck.txt
+++ b/dev-requirements/slotscheck.txt
@@ -1,1 +1,1 @@
-slotscheck==0.16.1
+slotscheck==0.16.2
diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py
index a420959a..12cf8d42 100644
--- a/hikari/impl/event_manager_base.py
+++ b/hikari/impl/event_manager_base.py
@@ -162,29 +162,6 @@ def __init__(
         # The registered wrapping function for the weak ref to this class's _listener method.
         self._timeout = timeout
 
-    # These are only included at runtime in-order to avoid the model being typed as an asynchronous context manager.
-    if not typing.TYPE_CHECKING:
-
-        async def __aenter__(self: event_manager_.EventStreamT) -> event_manager_.EventStreamT:
-            # This is sync only.
-            warnings.warn(
-                "Using EventStream as an async context manager has been deprecated since 2.0.0.dev104. "
-                "Please use it as a synchronous context manager (e.g. with bot.stream(...)) instead.",
-                category=DeprecationWarning,
-                stacklevel=2,
-            )
-
-            self.open()
-            return self
-
-        async def __aexit__(
-            self,
-            exc_type: typing.Optional[typing.Type[BaseException]],
-            exc: typing.Optional[BaseException],
-            exc_tb: typing.Optional[types.TracebackType],
-        ) -> None:
-            self.close()
-
     def __enter__(self: _EventStreamT) -> _EventStreamT:
         self.open()
         return self
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index d296e892..e6ca8364 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -449,8 +449,10 @@ def build(
             trust_env=proxy_settings.trust_env,
         )
         _LOGGER.log(ux.TRACE, "acquired new aiohttp client session")
+        buckets = buckets_impl.RESTBucketManager(max_rate_limit)
+        buckets.start()
         return _LiveAttributes(
-            buckets=buckets_impl.RESTBucketManager(max_rate_limit),
+            buckets=buckets,
             client_session=client_session,
             closed_event=asyncio.Event(),
             global_rate_limit=rate_limits.ManualRateLimiter(),
diff --git a/hikari/internal/aio.py b/hikari/internal/aio.py
index 37d34843..32a5860e 100644
--- a/hikari/internal/aio.py
+++ b/hikari/internal/aio.py
@@ -38,6 +38,7 @@
 import inspect
 import sys
 import typing
+import warnings
 
 if typing.TYPE_CHECKING:
     import logging
@@ -208,7 +209,9 @@ def get_or_make_loop() -> asyncio.AbstractEventLoop:
     # get_event_loop will error under oddly specific cases such as if set_event_loop has been called before even
     # if it was just called with None or if it's called on a thread which isn't the main Thread.
     try:
-        loop = asyncio.get_event_loop_policy().get_event_loop()
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore", DeprecationWarning)
+            loop = asyncio.get_event_loop_policy().get_event_loop()
 
         # Closed loops cannot be re-used.
         if not loop.is_closed():
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index cf1914c2..0e73dd72 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -149,7 +149,8 @@ def init_logging(
 
 def _read_banner(package: str) -> str:
     if sys.version_info >= (3, 9):
-        return importlib.resources.files(package).joinpath("banner.txt").open("r").read()
+        with importlib.resources.files(package).joinpath("banner.txt").open("r") as fp:
+            return fp.read()
     else:
         return importlib.resources.read_text(package, "banner.txt")
 
diff --git a/hikari/iterators.py b/hikari/iterators.py
index b73c76ef..52199fc4 100644
--- a/hikari/iterators.py
+++ b/hikari/iterators.py
@@ -724,6 +724,16 @@ def __await__(self) -> typing.Generator[None, None, typing.Sequence[ValueT]]:
     async def __anext__(self) -> ValueT:
         ...
 
+    # These are only included at runtime in-order to avoid the model being typed as a synchronous iterator.
+    if not typing.TYPE_CHECKING:
+
+        def __next__(self) -> typing.NoReturn:
+            # This is async only.
+            cls = type(self)
+            raise TypeError(
+                f"{cls.__module__}.{cls.__qualname__} is async-only, did you mean 'async for' or `anext`?"
+            ) from None
+
 
 class BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT], abc.ABC):
     """A special kind of lazy iterator that is used by internal components.


Test patch:
diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt
index 77d10d23..d79e8e13 100644
--- a/dev-requirements/pytest.txt
+++ b/dev-requirements/pytest.txt
@@ -1,5 +1,5 @@
 # mock in the stdlib changes between versions of Python, so are not consistent in behaviour like the backport is
-mock==4.0.3
+mock==5.0.0
 
 pytest==7.2.0
 pytest-asyncio==0.20.3
diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py
index 39de4f63..ad124c02 100644
--- a/tests/hikari/impl/test_event_manager_base.py
+++ b/tests/hikari/impl/test_event_manager_base.py
@@ -94,34 +94,6 @@ def test___enter___and___exit__(
         stub_stream.open.assert_called_once_with()
         stub_stream.close.assert_called_once_with()
 
-    @pytest.mark.asyncio()
-    async def test___aenter__(self):
-        # flake8 gets annoyed if we use "with" here so here's a hacky alternative
-        stub_stream = hikari_test_helpers.mock_class_namespace(
-            event_manager_base.EventStream, open=mock.Mock(), close=mock.Mock()
-        )(mock_app, base_events.Event, timeout=None)
-
-        with warnings.catch_warnings():
-            warnings.filterwarnings("ignore", category=DeprecationWarning)
-
-            async with stub_stream:
-                stub_stream.open.assert_called_once_with()
-                stub_stream.close.assert_not_called()
-
-        stub_stream.open.assert_called_once_with()
-        stub_stream.close.assert_called_once_with()
-
-    @pytest.mark.asyncio()
-    async def test___exit__(self):
-        stub_stream = hikari_test_helpers.mock_class_namespace(event_manager_base.EventStream)(
-            mock_app, base_events.Event, timeout=None
-        )
-
-        try:
-            await stub_stream.__aexit__(None, None, None)
-        except AttributeError as exc:
-            pytest.fail(exc)
-
     @pytest.mark.asyncio()
     async def test__listener_when_filter_returns_false(self, mock_app):
         stream = event_manager_base.EventStream(mock_app, base_events.Event, timeout=None)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 2af8b2f9..9d914ac0 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -320,6 +320,7 @@ def test_build(self):
         assert attributes.tcp_connector is create_tcp_connector.return_value
 
         bucket_manager.assert_called_once_with(123.321)
+        bucket_manager.return_value.start.assert_called_once_with()
         create_tcp_connector.assert_called_once_with(mock_settings)
         create_client_session.assert_called_once_with(
             connector=create_tcp_connector.return_value,
diff --git a/tests/hikari/internal/test_ux.py b/tests/hikari/internal/test_ux.py
index 5596a76e..c5226702 100644
--- a/tests/hikari/internal/test_ux.py
+++ b/tests/hikari/internal/test_ux.py
@@ -146,9 +146,25 @@ def test_when_doesnt_support_color(self):
 
 class TestRedBanner:
     def test_when_above_3_9(self):
+        class MockFile:
+            context_entered = 0
+            context_exited = 0
+
+            def __enter__(self):
+                self.context_entered += 1
+                return self
+
+            def __exit__(self, exc_type, exc_val, exc_tb):
+                self.context_exited += 1
+                return None
+
+            def read(self):
+                return read
+
         class MockTraversable:
             joint_path = None
             open_mode = None
+            mock_file = None
 
             def joinpath(self, path):
                 self.joint_path = path
@@ -156,12 +172,10 @@ def joinpath(self, path):
 
             def open(self, mode):
                 self.open_mode = mode
-                return self
-
-            def read(self):
-                return read
+                return self.mock_file
 
         traversable = MockTraversable()
+        traversable.mock_file = MockFile()
         read = object()
 
         with mock.patch.object(sys, "version_info", new=(3, 9)):
@@ -171,6 +185,8 @@ def read(self):
         read_text.assert_called_once_with("hikaru")
         assert traversable.joint_path == "banner.txt"
         assert traversable.open_mode == "r"
+        assert traversable.mock_file.context_entered == 1
+        assert traversable.mock_file.context_exited == 1
 
     def test_when_bellow_3_9(self):
         with mock.patch.object(sys, "version_info", new=(2, 7)):
diff --git a/tests/hikari/test_iterators.py b/tests/hikari/test_iterators.py
new file mode 100644
index 00000000..9a0c61cb
--- /dev/null
+++ b/tests/hikari/test_iterators.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Nekokatt
+# Copyright (c) 2021-present davfsa
+#
+# 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 pytest
+
+from hikari import iterators
+from tests.hikari import hikari_test_helpers
+
+
+class TestLazyIterator:
+    @pytest.fixture()
+    def lazy_iterator(self):
+        return hikari_test_helpers.mock_class_namespace(iterators.LazyIterator)()
+
+    def test_asynchronous_only(self, lazy_iterator):
+        with pytest.raises(TypeError, match="is async-only, did you mean 'async for' or `anext`?"):
+            next(lazy_iterator)