Repository: hikari-py/hikari
Pull request #1431 (2022-12-29 19:06:37)

Add a warning when not running in optimized mode
### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.



Patch:
diff --git a/changes/1378.deprecation.md b/changes/1378.deprecation.md
new file mode 100644
index 00000000..e5e59085
--- /dev/null
+++ b/changes/1378.deprecation.md
@@ -0,0 +1,1 @@
+Deprecate the `delete_message_days` parameter for PartialGuild.ban and Member.ban.
diff --git a/changes/1378.feature.md b/changes/1378.feature.md
new file mode 100644
index 00000000..2eb4cf57
--- /dev/null
+++ b/changes/1378.feature.md
@@ -0,0 +1,2 @@
+Allow specifying the `delete_message_seconds` parameter for PartialGuild.ban and Member.ban.
+- This parameter can be specified as either an int, a float, or a datetime.timedelta object.
diff --git a/changes/1420.feature.md b/changes/1420.feature.md
new file mode 100644
index 00000000..45a42e99
--- /dev/null
+++ b/changes/1420.feature.md
@@ -0,0 +1,4 @@
+Add missing `Application` fields:
+- `Application.custom_install_url`
+- `Application.tags`
+- `Application.install_parameters`
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/1431.feature.md b/changes/1431.feature.md
new file mode 100644
index 00000000..ad114190
--- /dev/null
+++ b/changes/1431.feature.md
@@ -0,0 +1,1 @@
+Add a warning when not running in (at least) level 1 optimization mode.
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/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/docs/_static/extra.css b/docs/_static/extra.css
index 33eb7004..7d0dd52d 100644
--- a/docs/_static/extra.css
+++ b/docs/_static/extra.css
@@ -24,6 +24,10 @@ body {
     right: -20em;
 }
 
+.toc-tree ul ul ul ul {
+    border-left: 1px solid var(--color-background-border);
+}
+
 @media (max-width: 82em) {
     body {
         font-size: 0.7em;
diff --git a/docs/conf.py b/docs/conf.py
index de81a24c..7988ec98 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -103,6 +103,7 @@
 autoapi_add_toctree_entry = False
 autoapi_add_objects_to_toctree = False
 autoapi_keep_files = True
+autoapi_member_order = "groupwise"
 
 # -- AutoDoc options ----------------------------------------------------------
 
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index 4bb207ce..553040fb 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -5926,9 +5926,10 @@ async def ban_user(
         user: snowflakes.SnowflakeishOr[users.PartialUser],
         *,
         delete_message_days: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+        delete_message_seconds: undefined.UndefinedOr[time.Intervalish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> None:
-        """Ban a member from a guild.
+        """Ban the given user from this guild.
 
         Parameters
         ----------
@@ -5944,6 +5945,13 @@ async def ban_user(
         delete_message_days : hikari.undefined.UndefinedOr[int]
             If provided, the number of days to delete messages for.
             This must be between 0 and 7.
+
+            .. deprecated:: 2.0.0.dev114
+                Use `delete_message_seconds` instead.
+        delete_message_seconds : hikari.undefined.UndefinedNoneOr[hikari.internal.time.Intervalish]
+            If provided, the number of seconds to delete messages for.
+            This can be represented as either an int/float between 0 and 604800 (7 days), or
+            a `datetime.timedelta` object.
         reason : hikari.undefined.UndefinedOr[str]
             If provided, the reason that will be recorded in the audit logs.
             Maximum of 512 characters.
@@ -5980,6 +5988,7 @@ async def ban_member(
         user: snowflakes.SnowflakeishOr[users.PartialUser],
         *,
         delete_message_days: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+        delete_message_seconds: undefined.UndefinedOr[time.Intervalish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> None:
         """Alias of `ban_user`."""
diff --git a/hikari/applications.py b/hikari/applications.py
index 50329802..f3b045c6 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -532,6 +532,18 @@ def make_cover_image_url(self, *, ext: str = "png", size: int = 4096) -> typing.
         )
 
 
+@attr_extensions.with_copy
+@attr.define(hash=True, kw_only=True, weakref_slot=False)
+class ApplicationInstallParameters:
+    """Represents the application install parameters."""
+
+    scopes: typing.Sequence[str] = attr.field(eq=True, repr=False, hash=True)
+    """The scopes to authorize the bot for."""
+
+    permissions: permissions_.Permissions = attr.field(eq=True, repr=False, hash=True)
+    """The permissions to add the bot to guild with."""
+
+
 @attr_extensions.with_copy
 @attr.define(hash=True, kw_only=True, weakref_slot=False)
 class Application(guilds.PartialApplication):
@@ -575,6 +587,15 @@ class Application(guilds.PartialApplication):
     privacy_policy_url: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False)
     """The URL of this application's privacy policy."""
 
+    custom_install_url: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False)
+    """The URL of this application's custom authorization link."""
+
+    tags: typing.Sequence[str] = attr.field(eq=False, hash=False, repr=False)
+    """A sequence of tags describing the content and functionality of the application."""
+
+    install_parameters: typing.Optional[ApplicationInstallParameters] = attr.field(eq=False, hash=False, repr=False)
+    """Settings for the application's default in-app authorization link, if enabled."""
+
     @property
     def cover_image_url(self) -> typing.Optional[files.URL]:
         """Rich presence cover image URL for this application, if set."""
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 0d52d910..88a059cd 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -66,6 +66,7 @@
 from hikari import urls
 from hikari import users
 from hikari.internal import attr_extensions
+from hikari.internal import deprecation
 from hikari.internal import enums
 from hikari.internal import routes
 from hikari.internal import time
@@ -691,6 +692,7 @@ async def ban(
         self,
         *,
         delete_message_days: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+        delete_message_seconds: undefined.UndefinedOr[time.Intervalish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> None:
         """Ban this member from this guild.
@@ -700,6 +702,13 @@ async def ban(
         delete_message_days : hikari.undefined.UndefinedOr[int]
             If provided, the number of days to delete messages for.
             This must be between 0 and 7.
+
+            .. deprecated:: 2.0.0.dev114
+                Use `delete_message_seconds` instead.
+        delete_message_seconds : hikari.undefined.UndefinedNoneOr[hikari.internal.time.Intervalish]
+            If provided, the number of seconds to delete messages for.
+            This can be represented as either an int/float between 0 and 604800 (7 days), or
+            a `datetime.timedelta` object.
         reason : hikari.undefined.UndefinedOr[str]
             If provided, the reason that will be recorded in the audit logs.
             Maximum of 512 characters.
@@ -728,8 +737,21 @@ async def ban(
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
         """
+        if delete_message_days is not undefined.UNDEFINED:
+            deprecation.warn_deprecated(
+                "delete_message_days",
+                removal_version="2.0.0.dev116",
+                additional_info="'delete_message_seconds' should be used instead.",
+            )
+            if delete_message_seconds is not undefined.UNDEFINED:
+                raise ValueError(
+                    "You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+                )
+
+            delete_message_seconds = delete_message_days * 24 * 60**2
+
         await self.user.app.rest.ban_user(
-            self.guild_id, self.user.id, delete_message_days=delete_message_days, reason=reason
+            self.guild_id, self.user.id, delete_message_seconds=delete_message_seconds, reason=reason
         )
 
     async def unban(
@@ -1451,6 +1473,7 @@ async def ban(
         user: snowflakes.SnowflakeishOr[users.PartialUser],
         *,
         delete_message_days: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+        delete_message_seconds: undefined.UndefinedOr[time.Intervalish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> None:
         """Ban the given user from this guild.
@@ -1465,6 +1488,13 @@ async def ban(
         delete_message_days : hikari.undefined.UndefinedOr[int]
             If provided, the number of days to delete messages for.
             This must be between 0 and 7.
+
+            .. deprecated:: 2.0.0.dev114
+                Use `delete_message_seconds` instead.
+        delete_message_seconds : hikari.undefined.UndefinedNoneOr[hikari.internal.time.Intervalish]
+            If provided, the number of seconds to delete messages for.
+            This can be represented as either an int/float between 0 and 604800 (7 days), or
+            a `datetime.timedelta` object.
         reason : hikari.undefined.UndefinedOr[str]
             If provided, the reason that will be recorded in the audit logs.
             Maximum of 512 characters.
@@ -1493,7 +1523,20 @@ async def ban(
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
         """
-        await self.app.rest.ban_user(self.id, user, delete_message_days=delete_message_days, reason=reason)
+        if delete_message_days is not undefined.UNDEFINED:
+            deprecation.warn_deprecated(
+                "delete_message_days",
+                removal_version="2.0.0.dev116",
+                additional_info="'delete_message_seconds' should be used instead.",
+            )
+            if delete_message_seconds is not undefined.UNDEFINED:
+                raise ValueError(
+                    "You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+                )
+
+            delete_message_seconds = delete_message_days * 24 * 60**2
+
+        await self.app.rest.ban_user(self.id, user, delete_message_seconds=delete_message_seconds, reason=reason)
 
     async def unban(
         self,
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index 01e948c1..70666711 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -296,6 +296,7 @@ def __init__(
         # Beautification and logging
         ux.init_logging(logs, allow_color, force_color)
         self.print_banner(banner, allow_color, force_color)
+        ux.warn_if_not_optimized()
 
         # Settings and state
         self._closed_event: typing.Optional[asyncio.Event] = None
@@ -669,7 +670,7 @@ def run(
         shard_ids: typing.Optional[typing.Sequence[int]] = None,
         shard_count: typing.Optional[int] = None,
     ) -> None:
-        """Start the bot, wait for all shards to become ready, and then return.
+        """Start the application and block until it's finished running.
 
         Other Parameters
         ----------------
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 750fca76..59dce868 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -609,6 +609,13 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati
                 owner_id=snowflakes.Snowflake(team_payload["owner_user_id"]),
             )
 
+        install_parameters: typing.Optional[application_models.ApplicationInstallParameters] = None
+        if (install_payload := payload.get("install_params")) is not None:
+            install_parameters = application_models.ApplicationInstallParameters(
+                scopes=[application_models.OAuth2Scope(scope) for scope in install_payload["scopes"]],
+                permissions=permission_models.Permissions(install_payload["permissions"]),
+            )
+
         return application_models.Application(
             app=self._app,
             id=snowflakes.Snowflake(payload["id"]),
@@ -625,6 +632,9 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati
             cover_image_hash=payload.get("cover_image"),
             privacy_policy_url=payload.get("privacy_policy_url"),
             terms_of_service_url=payload.get("terms_of_service_url"),
+            custom_install_url=payload.get("custom_install_url"),
+            tags=payload.get("tags") or [],
+            install_parameters=install_parameters,
         )
 
     def deserialize_authorization_information(
@@ -1406,7 +1416,6 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em
     def serialize_embed(  # noqa: C901 - Function too complex
         self, embed: embed_models.Embed
     ) -> typing.Tuple[data_binding.JSONObject, typing.List[files.Resource[files.AsyncReader]]]:
-
         payload: typing.Dict[str, typing.Any] = {}
         uploads: typing.List[files.Resource[files.AsyncReader]] = []
 
@@ -1476,7 +1485,6 @@ def serialize_embed(  # noqa: C901 - Function too complex
         if embed.fields:
             field_payloads: typing.List[data_binding.JSONObject] = []
             for i, field in enumerate(embed.fields):
-
                 # Yep, these are technically two unreachable branches. However, this is an incredibly
                 # common mistake to make when working with embeds and not using a static type
                 # checker, so I have added these as additional safeguards for UX and ease
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..2d3ad1be 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(),
@@ -2114,7 +2116,6 @@ def fetch_audit_log(
         user: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED,
         event_type: undefined.UndefinedOr[typing.Union[audit_logs.AuditLogEventType, int]] = undefined.UNDEFINED,
     ) -> iterators.LazyIterator[audit_logs.AuditLog]:
-
         timestamp: undefined.UndefinedOr[str]
         if before is undefined.UNDEFINED:
             timestamp = undefined.UNDEFINED
@@ -2933,10 +2934,27 @@ async def ban_user(
         user: snowflakes.SnowflakeishOr[users.PartialUser],
         *,
         delete_message_days: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+        delete_message_seconds: undefined.UndefinedOr[time.Intervalish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> None:
+        if delete_message_days is not undefined.UNDEFINED:
+            deprecation.warn_deprecated(
+                "delete_message_days",
+                removal_version="2.0.0.dev116",
+                additional_info="'delete_message_seconds' should be used instead.",
+            )
+            if delete_message_seconds:
+                raise ValueError(
+                    "You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+                )
+
+            delete_message_seconds = delete_message_days * 24 * 60**2
+
+        if isinstance(delete_message_seconds, datetime.timedelta):
+            delete_message_seconds = delete_message_seconds.total_seconds()
+
         body = data_binding.JSONObjectBuilder()
-        body.put("delete_message_days", delete_message_days)
+        body.put("delete_message_seconds", delete_message_seconds)
         route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user)
         await self._request(route, json=body, reason=reason)
 
@@ -2946,9 +2964,26 @@ def ban_member(
         user: snowflakes.SnowflakeishOr[users.PartialUser],
         *,
         delete_message_days: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+        delete_message_seconds: undefined.UndefinedOr[time.Intervalish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> typing.Coroutine[typing.Any, typing.Any, None]:
-        return self.ban_user(guild, user, delete_message_days=delete_message_days, reason=reason)
+        if delete_message_days is not undefined.UNDEFINED:
+            deprecation.warn_deprecated(
+                "delete_message_days",
+                removal_version="2.0.0.dev116",
+                additional_info="'delete_message_seconds' should be used instead.",
+            )
+            if delete_message_seconds:
+                raise ValueError(
+                    "You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+                )
+
+            delete_message_seconds = delete_message_days * 24 * 60**2
+
+        if isinstance(delete_message_seconds, datetime.timedelta):
+            delete_message_seconds = delete_message_seconds.total_seconds()
+
+        return self.ban_user(guild, user, delete_message_seconds=delete_message_seconds, reason=reason)
 
     async def unban_user(
         self,
diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py
index fe817463..24eb8e3c 100644
--- a/hikari/impl/rest_bot.py
+++ b/hikari/impl/rest_bot.py
@@ -262,6 +262,7 @@ def __init__(
         # Beautification and logging
         ux.init_logging(logs, allow_color, force_color)
         self.print_banner(banner, allow_color, force_color)
+        ux.warn_if_not_optimized()
 
         # Settings and state
         self._close_event: typing.Optional[asyncio.Event] = None
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/time.py b/hikari/internal/time.py
index 17c1df0a..9a264e9c 100644
--- a/hikari/internal/time.py
+++ b/hikari/internal/time.py
@@ -204,7 +204,7 @@ def utc_datetime() -> datetime.datetime:
 # monotonic on ARM64 architectures, but on x86, monotonic is around 1ns faster
 # than monotonic_ns. Just thought that was kind of interesting to note down.
 # (RPi 3B versus i7 6700)
-
+#
 # time.perf_counter and time.perf_counter_ns don't have proper typehints, causing
 # pdoc to not be able to recognise them. This is just a little hack around that.
 def monotonic() -> float:
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index cf1914c2..5fa0985a 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -23,7 +23,14 @@
 """User-experience extensions and utilities."""
 from __future__ import annotations
 
-__all__: typing.Sequence[str] = ("init_logging", "print_banner", "supports_color", "HikariVersion", "check_for_updates")
+__all__: typing.Sequence[str] = (
+    "init_logging",
+    "print_banner",
+    "warn_if_not_optimized",
+    "supports_color",
+    "HikariVersion",
+    "check_for_updates",
+)
 
 import importlib.resources
 import logging
@@ -149,7 +156,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")
 
@@ -233,6 +241,15 @@ def print_banner(
         stdout.write(string.Template(raw_banner).safe_substitute(args))
 
 
+def warn_if_not_optimized() -> None:
+    """Log a warning if not running in optimization mode."""
+    if __debug__:
+        _LOGGER.warning(
+            "You are running on optimization level 0 (no optimizations), which may slow down your application. "
+            "For production, consider using at least level 1 optimization by passing `-O` to the python call."
+        )
+
+
 def supports_color(allow_color: bool, force_color: bool) -> bool:
     """Return `True` if the terminal device supports color output.
 
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.
diff --git a/hikari/users.py b/hikari/users.py
index 0c21eef0..5b94a6a2 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -148,15 +148,13 @@ def avatar_hash(self) -> undefined.UndefinedNoneOr[str]:
     @property
     @abc.abstractmethod
     def banner_hash(self) -> undefined.UndefinedNoneOr[str]:
-        """Banner hash for the user, if they have one, otherwise `hikari.undefined.UNDEFINED`."""
+        """Banner hash for the user, if they have one, otherwise `None`."""
 
     @property
     @abc.abstractmethod
     def accent_color(self) -> undefined.UndefinedNoneOr[colors.Color]:
         """Custom banner color for the user if set, else `None`.
 
-        Will be `hikari.undefined.UNDEFINED` if not known.
-
         The official client will decide the default color if not set.
         """  # noqa: D401 - Imperative mood
 
@@ -489,7 +487,7 @@ def avatar_url(self) -> typing.Optional[files.URL]:
     @property
     @abc.abstractmethod
     def banner_hash(self) -> typing.Optional[str]:
-        """Banner hash for the user, if they have one, otherwise `hikari.undefined.UNDEFINED`."""
+        """Banner hash for the user, if they have one, otherwise `None`."""
 
     @property
     def banner_url(self) -> typing.Optional[files.URL]:
@@ -652,7 +650,7 @@ class PartialUserImpl(PartialUser):
     """Implementation for partial information about a user.
 
     This is pretty much the same as a normal user, but information may not be
-    present.
+    present, which will be denoted by `hikari.undefined.UNDEFINED`.
     """
 
     id: snowflakes.Snowflake = attr.field(hash=True, repr=True)


Test patch:
diff --git a/tests/hikari/impl/test_bot.py b/tests/hikari/impl/test_bot.py
index 7c37ca11..ff130973 100644
--- a/tests/hikari/impl/test_bot.py
+++ b/tests/hikari/impl/test_bot.py
@@ -156,6 +156,7 @@ def bot(
         stack.enter_context(mock.patch.object(rest_impl, "RESTClientImpl", return_value=rest))
         stack.enter_context(mock.patch.object(ux, "init_logging"))
         stack.enter_context(mock.patch.object(bot_impl.GatewayBot, "print_banner"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
 
         with stack:
             return bot_impl.GatewayBot(
@@ -176,6 +177,7 @@ def test_init(self):
         voice = stack.enter_context(mock.patch.object(voice_impl, "VoiceComponentImpl"))
         rest = stack.enter_context(mock.patch.object(rest_impl, "RESTClientImpl"))
         init_logging = stack.enter_context(mock.patch.object(ux, "init_logging"))
+        warn_if_not_optimized = stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         print_banner = stack.enter_context(mock.patch.object(bot_impl.GatewayBot, "print_banner"))
         executor = object()
         cache_settings = object()
@@ -235,6 +237,7 @@ def test_init(self):
 
         init_logging.assert_called_once_with("DEBUG", False, True)
         print_banner.assert_called_once_with("testing", False, True)
+        warn_if_not_optimized.assert_called_once_with()
 
     def test_init_when_no_settings(self):
         stack = contextlib.ExitStack()
@@ -246,6 +249,7 @@ def test_init_when_no_settings(self):
         stack.enter_context(mock.patch.object(rest_impl, "RESTClientImpl"))
         stack.enter_context(mock.patch.object(ux, "init_logging"))
         stack.enter_context(mock.patch.object(bot_impl.GatewayBot, "print_banner"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         http_settings = stack.enter_context(mock.patch.object(config, "HTTPSettings"))
         proxy_settings = stack.enter_context(mock.patch.object(config, "ProxySettings"))
         cache_settings = stack.enter_context(mock.patch.object(config, "CacheSettings"))
@@ -264,6 +268,7 @@ def test_init_strips_token(self):
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(ux, "init_logging"))
         stack.enter_context(mock.patch.object(bot_impl.GatewayBot, "print_banner"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
 
         with stack:
             bot = bot_impl.GatewayBot(
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index ca558ff0..23ccf860 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -948,6 +948,12 @@ def application_payload(self, owner_payload, user_payload):
             "cover_image": "hashmebaby",
             "privacy_policy_url": "hahaha://hahaha",
             "terms_of_service_url": "haha2:2h2h2h2",
+            "custom_install_url": "https://dontinstallme.com",
+            "tags": ["i", "like", "hikari"],
+            "install_params": {
+                "scopes": ["bot", "applications.commands"],
+                "permissions": 8,
+            },
         }
 
     def test_deserialize_application(
@@ -970,7 +976,16 @@ def test_deserialize_application(
         assert application.flags == application_models.ApplicationFlags.VERIFICATION_PENDING_GUILD_LIMIT
         assert application.privacy_policy_url == "hahaha://hahaha"
         assert application.terms_of_service_url == "haha2:2h2h2h2"
+        assert application.custom_install_url == "https://dontinstallme.com"
+        assert application.tags == ["i", "like", "hikari"]
         assert application.icon_hash == "iwiwiwiwiw"
+        # Install Parameters
+        assert application.install_parameters.scopes == [
+            application_models.OAuth2Scope.BOT,
+            application_models.OAuth2Scope.APPLICATIONS_COMMANDS,
+        ]
+        assert application.install_parameters.permissions == permission_models.Permissions.ADMINISTRATOR
+        assert isinstance(application.install_parameters, application_models.ApplicationInstallParameters)
         # Team
         assert application.team.id == 202020202
         assert application.team.name == "Hikari Development"
@@ -1028,8 +1043,13 @@ def test_deserialize_application_with_null_fields(self, entity_factory_impl, moc
 
         assert application.description is None
         assert application.icon_hash is None
+        assert application.terms_of_service_url is None
+        assert application.privacy_policy_url is None
+        assert application.custom_install_url is None
         assert application.cover_image_hash is None
         assert application.team is None
+        assert application.install_parameters is None
+        assert application.tags == []
 
     @pytest.fixture()
     def invite_application_payload(self):
@@ -6223,7 +6243,7 @@ def test_deserialize_scheduled_event_returns_right_type(
         scheduled_stage_event_payload: typing.Dict[str, typing.Any],
         scheduled_voice_event_payload: typing.Dict[str, typing.Any],
     ):
-        for (cls, payload) in [
+        for cls, payload in [
             (scheduled_event_models.ScheduledExternalEvent, scheduled_external_event_payload),
             (scheduled_event_models.ScheduledStageEvent, scheduled_stage_event_payload),
             (scheduled_event_models.ScheduledVoiceEvent, scheduled_voice_event_payload),
diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py
index 39de4f63..b4f6859e 100644
--- a/tests/hikari/impl/test_event_manager_base.py
+++ b/tests/hikari/impl/test_event_manager_base.py
@@ -23,6 +23,7 @@
 
 import asyncio
 import contextlib
+import gc
 import logging
 import sys
 import typing
@@ -94,34 +95,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)
@@ -243,6 +216,7 @@ def test___del___for_active_stream(self):
 
         with unittest.TestCase().assertLogs("hikari.event_manager", level=logging.WARNING) as logging_watcher:
             del streamer
+            gc.collect()  # Force a GC collection
 
         assert logging_watcher.output == [
             "WARNING:hikari.event_manager:active 'Event' streamer fell out of scope before being closed"
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 2af8b2f9..6e7c6dc0 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,
@@ -1122,10 +1123,25 @@ def test_ban_member(self, rest_client):
         mock_ban_user = mock.Mock()
         rest_client.ban_user = mock_ban_user
 
-        result = rest_client.ban_member(43123, 54123, delete_message_days=6, reason="wowowowo")
+        result = rest_client.ban_member(43123, 54123, delete_message_seconds=518400, reason="wowowowo")
 
         assert result is mock_ban_user.return_value
-        mock_ban_user.assert_called_once_with(43123, 54123, delete_message_days=6, reason="wowowowo")
+        mock_ban_user.assert_called_once_with(43123, 54123, delete_message_seconds=518400, reason="wowowowo")
+
+    def test_ban_member_deprecated_delete_message_days(self, rest_client):
+        mock_ban_user = mock.Mock()
+        rest_client.ban_user = mock_ban_user
+
+        result = rest_client.ban_member(43123, 54123, delete_message_days=1, reason="wowowowo")
+
+        assert result is mock_ban_user.return_value
+        mock_ban_user.assert_called_once_with(43123, 54123, delete_message_seconds=86400, reason="wowowowo")
+
+    def test_ban_member_deprecated_delete_message_days_when_both_passed(self, rest_client):
+        with pytest.raises(
+            ValueError, match="You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+        ):
+            rest_client.ban_member(43123, 54123, delete_message_days=1, delete_message_seconds=1, reason="wowowowo")
 
     def test_unban_member(self, rest_client):
         mock_unban_user = mock.Mock()
@@ -4824,13 +4840,32 @@ async def test_kick_user(self, rest_client):
 
     async def test_ban_user(self, rest_client):
         expected_route = routes.PUT_GUILD_BAN.compile(guild=123, user=456)
-        expected_json = {"delete_message_days": 7}
+        expected_json = {"delete_message_seconds": 604800}
+        rest_client._request = mock.AsyncMock()
+
+        await rest_client.ban_user(
+            StubModel(123), StubModel(456), delete_message_seconds=604800, reason="because i can"
+        )
+
+        rest_client._request.assert_awaited_once_with(expected_route, json=expected_json, reason="because i can")
+
+    async def test_ban_user_deprecated_delete_message_days(self, rest_client):
+        expected_route = routes.PUT_GUILD_BAN.compile(guild=123, user=456)
+        expected_json = {"delete_message_seconds": 86400}
         rest_client._request = mock.AsyncMock()
 
-        await rest_client.ban_user(StubModel(123), StubModel(456), delete_message_days=7, reason="because i can")
+        await rest_client.ban_user(StubModel(123), StubModel(456), delete_message_days=1, reason="because i can")
 
         rest_client._request.assert_awaited_once_with(expected_route, json=expected_json, reason="because i can")
 
+    async def test_ban_user_deprecated_delete_message_days_when_both_passed(self, rest_client):
+        with pytest.raises(
+            ValueError, match="You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+        ):
+            await rest_client.ban_user(
+                43123, 54123, delete_message_seconds=1, delete_message_days=1, reason="because i can"
+            )
+
     async def test_unban_user(self, rest_client):
         expected_route = routes.DELETE_GUILD_BAN.compile(guild=123, user=456)
         rest_client._request = mock.AsyncMock()
diff --git a/tests/hikari/impl/test_rest_bot.py b/tests/hikari/impl/test_rest_bot.py
index 6157dbf6..ef60c4fc 100644
--- a/tests/hikari/impl/test_rest_bot.py
+++ b/tests/hikari/impl/test_rest_bot.py
@@ -76,6 +76,7 @@ def mock_rest_bot(
     ):
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(ux, "init_logging"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         stack.enter_context(mock.patch.object(rest_bot_impl.RESTBot, "print_banner"))
         stack.enter_context(
             mock.patch.object(entity_factory_impl, "EntityFactoryImpl", return_value=mock_entity_factory)
@@ -102,6 +103,7 @@ def test___init__(
 
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(ux, "init_logging"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         stack.enter_context(mock.patch.object(rest_bot_impl.RESTBot, "print_banner"))
         stack.enter_context(
             mock.patch.object(entity_factory_impl, "EntityFactoryImpl", return_value=mock_entity_factory)
@@ -159,6 +161,7 @@ def test___init___parses_string_public_key(self):
 
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(ux, "init_logging"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         stack.enter_context(mock.patch.object(rest_bot_impl.RESTBot, "print_banner"))
         stack.enter_context(mock.patch.object(interaction_server_impl, "InteractionServer"))
 
@@ -174,6 +177,7 @@ def test___init___strips_token(self):
 
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(ux, "init_logging"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         rest_client = stack.enter_context(mock.patch.object(rest_impl, "RESTClientImpl"))
         http_settings = stack.enter_context(mock.patch.object(config, "HTTPSettings"))
         proxy_settings = stack.enter_context(mock.patch.object(config, "ProxySettings"))
@@ -199,6 +203,7 @@ def test___init___generates_default_settings(self):
         cls = hikari_test_helpers.mock_class_namespace(rest_bot_impl.RESTBot, print_banner=mock.Mock())
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(ux, "init_logging"))
+        stack.enter_context(mock.patch.object(ux, "warn_if_not_optimized"))
         stack.enter_context(mock.patch.object(rest_bot_impl.RESTBot, "print_banner"))
         stack.enter_context(mock.patch.object(rest_impl, "RESTClientImpl"))
         stack.enter_context(mock.patch.object(config, "HTTPSettings"))
diff --git a/tests/hikari/internal/test_ux.py b/tests/hikari/internal/test_ux.py
index 5596a76e..7a0dad2c 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)):
@@ -356,6 +372,22 @@ def test_overwrite_args_raises_error(self, mock_args):
                 ux.print_banner("hikari", True, False, extra_args=extra_args)
 
 
+class TestWarnIfNotOptimized:
+    @pytest.mark.skipif(not __debug__, reason="Not running in optimized mode")
+    def test_when_not_optimized(self):
+        with mock.patch.object(ux, "_LOGGER") as logger:
+            ux.warn_if_not_optimized()
+
+        logger.warning.assert_called()
+
+    @pytest.mark.skipif(__debug__, reason="Running in optimized mode")
+    def test_when_optimized(self):
+        with mock.patch.object(ux, "_LOGGER") as logger:
+            ux.warn_if_not_optimized()
+
+        logger.warning.assert_not_called()
+
+
 class TestSupportsColor:
     def test_when_not_allow_color(self):
         assert ux.supports_color(False, True) is False
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index de4d3db4..3f15ecef 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -388,9 +388,24 @@ async def test_fetch_roles(self, model):
     async def test_ban(self, model):
         model.app.rest.ban_user = mock.AsyncMock()
 
-        await model.ban(delete_message_days=10, reason="bored")
+        await model.ban(delete_message_seconds=600, reason="bored")
 
-        model.app.rest.ban_user.assert_awaited_once_with(456, 123, delete_message_days=10, reason="bored")
+        model.app.rest.ban_user.assert_awaited_once_with(456, 123, delete_message_seconds=600, reason="bored")
+
+    @pytest.mark.asyncio()
+    async def test_ban_deprecated_delete_message_days(self, model):
+        model.app.rest.ban_user = mock.AsyncMock()
+
+        await model.ban(delete_message_days=1, reason="bored")
+
+        model.app.rest.ban_user.assert_awaited_once_with(456, 123, delete_message_seconds=86400, reason="bored")
+
+    @pytest.mark.asyncio()
+    async def test_ban_deprecated_delete_message_days_when_both_passed(self, model):
+        with pytest.raises(
+            ValueError, match="You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+        ):
+            await model.ban(delete_message_days=1, delete_message_seconds=1, reason="bored")
 
     @pytest.mark.asyncio()
     async def test_unban(self, model):
@@ -624,9 +639,25 @@ async def test_kick(self, model):
     @pytest.mark.asyncio()
     async def test_ban(self, model):
         model.app.rest.ban_user = mock.AsyncMock()
-        await model.ban(4321, delete_message_days=10, reason="Go away!")
 
-        model.app.rest.ban_user.assert_awaited_once_with(90210, 4321, delete_message_days=10, reason="Go away!")
+        await model.ban(4321, delete_message_seconds=864000, reason="Go away!")
+
+        model.app.rest.ban_user.assert_awaited_once_with(90210, 4321, delete_message_seconds=864000, reason="Go away!")
+
+    @pytest.mark.asyncio()
+    async def test_ban_deprecated_delete_message_days(self, model):
+        model.app.rest.ban_user = mock.AsyncMock()
+
+        await model.ban(4321, delete_message_days=1, reason="Go away!")
+
+        model.app.rest.ban_user.assert_awaited_once_with(90210, 4321, delete_message_seconds=86400, reason="Go away!")
+
+    @pytest.mark.asyncio()
+    async def test_ban_deprecated_delete_message_days_when_both_passed(self, model):
+        with pytest.raises(
+            ValueError, match="You may only specify one of 'delete_message_days' or 'delete_message_seconds', not both"
+        ):
+            await model.ban(4321, delete_message_days=1, delete_message_seconds=1, reason="Go away!")
 
     @pytest.mark.asyncio()
     async def test_unban(self, model):
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)