Repository: hikari-py/hikari
Pull request #2151 (2025-01-29 13:07:56)

Bump pytest-asyncio from 0.25.0 to 0.25.3
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.25.0 to 0.25.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/pytest-dev/pytest-asyncio/releases">pytest-asyncio's releases</a>.</em></p>
<blockquote>
<h2>pytest-asyncio 0.25.3</h2>
<ul>
<li>Avoid errors in cleanup of async generators when event loop is already closed <a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1040">#1040</a></li>
</ul>
<h2>pytest-asyncio 0.25.2</h2>
<ul>
<li>Call <code>loop.shutdown_asyncgens()</code> before closing the event loop to ensure async generators are closed in the same manner as <code>asyncio.run</code> does <a href="https://redirect.github.com/pytest-dev/pytest-asyncio/pull/1034">#1034</a></li>
</ul>
<h2>pytest-asyncio 0.25.1</h2>
<ul>
<li>Fixes an issue that caused a broken event loop when a function-scoped test was executed in between two tests with wider loop scope <a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/950">#950</a></li>
<li>Improves test collection speed in auto mode <a href="https://redirect.github.com/pytest-dev/pytest-asyncio/pull/1020">#1020</a></li>
<li>Corrects the warning that is emitted upon redefining the event_loop fixture</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/7c501923b00b4aeeb68d4b3b11065f0c3b5cba2c"><code>7c50192</code></a> fix: Avoid errors in cleanup of async generators when event loop is already c...</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/2188cdbbf7b47e653957d3f39aeb7d4d0de80bf4"><code>2188cdb</code></a> build: Prepare release of v0.25.2.</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/c3ad6340b8ad251f3c03d6bbe399f08434059bda"><code>c3ad634</code></a> fix: Shutdown generators before closing event loops.</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/e8ffb10528aaf578dc9ccc7ddbb1fadab56be8ac"><code>e8ffb10</code></a> [pre-commit.ci] pre-commit autoupdate</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/aae43d4f766dfd8f9a643faad41be411888a5d16"><code>aae43d4</code></a> Build(deps): Bump hypothesis in /dependencies/default</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/941e8b5104d86bb1b06b15fd3395f23120203c09"><code>941e8b5</code></a> Build(deps): Bump pygments from 2.18.0 to 2.19.1 in /dependencies/docs</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/623ab74b80b6f748e88254714f9b21a16eb7911e"><code>623ab74</code></a> docs: Prepare release of v0.25.1.</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/c236550e739b55ee62f3e6ec7f71b4f4231236d1"><code>c236550</code></a> docs: Fix broken link to the pytest.mark.asyncio reference.</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/41c645b3b79709c2812a3aa3b17943074bc33f48"><code>41c645b</code></a> fix: Correct warning message when redefining the event_loop fixture.</li>
<li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/2fd10f824302d45da752341c60c8ddec48297a1b"><code>2fd10f8</code></a> docs: Clarify deprecation of event_loop fixture.</li>
<li>Additional commits viewable in <a href="https://github.com/pytest-dev/pytest-asyncio/compare/v0.25.0...v0.25.3">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-asyncio&package-manager=pip&previous-version=0.25.0&new-version=0.25.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/changes/2149.bugfix.md b/changes/2149.bugfix.md
new file mode 100644
index 00000000..987511b7
--- /dev/null
+++ b/changes/2149.bugfix.md
@@ -0,0 +1,1 @@
+Fixed Entitlement `starts_at` and `ends_at` erroring when set to `None`.
diff --git a/dev-requirements.txt b/dev-requirements.txt
index cbfbbfc0..4cb19302 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,1 +1,1 @@
-nox[uv]==2024.10.9
+nox[uv]==2025.2.9
diff --git a/dev-requirements/coverage.txt b/dev-requirements/coverage.txt
index e668f7d1..5f600886 100644
--- a/dev-requirements/coverage.txt
+++ b/dev-requirements/coverage.txt
@@ -1,1 +1,1 @@
-coverage[toml]==7.6.10
+coverage[toml]==7.6.12
diff --git a/dev-requirements/flake8.txt b/dev-requirements/flake8.txt
index a89447cf..6ea1ee49 100644
--- a/dev-requirements/flake8.txt
+++ b/dev-requirements/flake8.txt
@@ -15,7 +15,7 @@ flake8-mutable==1.2.0                   # mutable default argument detection
 flake8-pep3101==2.1.0                   # new-style format strings only
 flake8-print==5.0.0                     # complain about print statements in code
 flake8-printf-formatting==1.1.2         # forbey printf-style python2 string formatting
-flake8-pytest-style==2.0.0              # pytest checks
+flake8-pytest-style==2.1.0              # pytest checks
 flake8-raise==0.0.5                     # exception raising linting
 flake8-use-fstring==1.4                 # format string checking
 flake8-noqa==1.4.0                      # validate noqa commands
diff --git a/dev-requirements/mkdocs.txt b/dev-requirements/mkdocs.txt
index 19f1221f..cd8fdaa0 100644
--- a/dev-requirements/mkdocs.txt
+++ b/dev-requirements/mkdocs.txt
@@ -1,7 +1,7 @@
 mkdocs==1.6.1
 
 # Mkdocs Material
-mkdocs-material[imaging]==9.5.49
+mkdocs-material[imaging]==9.6.3
 
 # Docstring parsing for API reference
 mkdocstrings[python]==0.27.0
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index 66f07fd2..7b1b0698 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.392.post0
+pyright==1.1.394
diff --git a/dev-requirements/release.txt b/dev-requirements/release.txt
index 1d86da8e..b441f943 100644
--- a/dev-requirements/release.txt
+++ b/dev-requirements/release.txt
@@ -1,3 +1,3 @@
-twine==6.0.1
+twine==6.1.0
 
 -r build.txt
diff --git a/dev-requirements/ruff.txt b/dev-requirements/ruff.txt
index cd4e5668..b3f06b14 100644
--- a/dev-requirements/ruff.txt
+++ b/dev-requirements/ruff.txt
@@ -1,1 +1,1 @@
-ruff==0.8.4
+ruff==0.9.7
diff --git a/hikari/channels.py b/hikari/channels.py
index 10175f7b..72d5af2d 100644
--- a/hikari/channels.py
+++ b/hikari/channels.py
@@ -1193,9 +1193,9 @@ async def edit_overwrite(
             If an internal error occurs on Discord while handling the request.
         """
         if target_type is undefined.UNDEFINED:
-            assert not isinstance(
-                target, int
-            ), "Cannot determine the type of the target to update. Try specifying 'target_type' manually."
+            assert not isinstance(target, int), (
+                "Cannot determine the type of the target to update. Try specifying 'target_type' manually."
+            )
             return await self.app.rest.edit_permission_overwrite(self.id, target, allow=allow, deny=deny, reason=reason)
 
         return await self.app.rest.edit_permission_overwrite(
diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py
index a44bb562..438f41ca 100644
--- a/hikari/events/message_events.py
+++ b/hikari/events/message_events.py
@@ -191,9 +191,9 @@ def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
             return None
 
         channel = self.app.cache.get_guild_channel(self.channel_id)
-        assert channel is None or isinstance(
-            channel, channels.TextableGuildChannel
-        ), f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        assert channel is None or isinstance(channel, channels.TextableGuildChannel), (
+            f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        )
         return channel
 
     def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
@@ -429,9 +429,9 @@ def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
             return None
 
         channel = self.app.cache.get_guild_channel(self.channel_id)
-        assert channel is None or isinstance(
-            channel, channels.TextableGuildChannel
-        ), f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        assert channel is None or isinstance(channel, channels.TextableGuildChannel), (
+            f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        )
         return channel
 
     def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
@@ -544,9 +544,9 @@ def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
             return None
 
         channel = self.app.cache.get_guild_channel(self.channel_id)
-        assert channel is None or isinstance(
-            channel, channels.TextableGuildChannel
-        ), f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        assert channel is None or isinstance(channel, channels.TextableGuildChannel), (
+            f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        )
         return channel
 
     def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
@@ -640,9 +640,9 @@ def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
             return None
 
         channel = self.app.cache.get_guild_channel(self.channel_id)
-        assert channel is None or isinstance(
-            channel, channels.TextableGuildChannel
-        ), f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        assert channel is None or isinstance(channel, channels.TextableGuildChannel), (
+            f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        )
         return channel
 
     def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
diff --git a/hikari/events/typing_events.py b/hikari/events/typing_events.py
index 09345e3a..76bdba07 100644
--- a/hikari/events/typing_events.py
+++ b/hikari/events/typing_events.py
@@ -167,9 +167,9 @@ async def fetch_channel(self) -> channels.TextableGuildChannel:
             The channel.
         """
         channel = await super().fetch_channel()
-        assert isinstance(
-            channel, channels.TextableGuildChannel
-        ), f"expected TextableGuildChannel from API, got {channel}"
+        assert isinstance(channel, channels.TextableGuildChannel), (
+            f"expected TextableGuildChannel from API, got {channel}"
+        )
         return channel
 
     async def fetch_guild(self) -> guilds.Guild:
@@ -214,9 +214,9 @@ def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
             return None
 
         channel = self.app.cache.get_guild_channel(self.channel_id)
-        assert channel is None or isinstance(
-            channel, channels.TextableGuildChannel
-        ), f"expected TextableGuildChannel from cache, got {channel}"
+        assert channel is None or isinstance(channel, channels.TextableGuildChannel), (
+            f"expected TextableGuildChannel from cache, got {channel}"
+        )
         return channel
 
     def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index bb364022..a81fe8d4 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -3755,7 +3755,7 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_model
     ##################
 
     def deserialize_entitlement(self, payload: data_binding.JSONObject) -> monetization_models.Entitlement:
-        starts_at = time.iso8601_datetime_string_to_datetime(payload["starts_at"]) if "starts_at" in payload else None
+        starts_at = time.iso8601_datetime_string_to_datetime(payload["starts_at"]) if payload.get("starts_at") else None
 
         return monetization_models.Entitlement(
             id=snowflakes.Snowflake(payload["id"]),
@@ -3766,7 +3766,7 @@ def deserialize_entitlement(self, payload: data_binding.JSONObject) -> monetizat
             user_id=snowflakes.Snowflake(payload["user_id"]) if "user_id" in payload else None,
             is_deleted=payload["deleted"],
             starts_at=starts_at,
-            ends_at=time.iso8601_datetime_string_to_datetime(payload["ends_at"]) if "ends_at" in payload else None,
+            ends_at=time.iso8601_datetime_string_to_datetime(payload["ends_at"]) if payload.get("ends_at") else None,
             subscription_id=snowflakes.Snowflake(payload["subscription_id"]) if "subscription_id" in payload else None,
         )
 
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index 515015d0..6fc68f73 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -96,9 +96,9 @@ def deserialize_guild_channel_create_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
     ) -> channel_events.GuildChannelCreateEvent:
         channel = self._app.entity_factory.deserialize_channel(payload)
-        assert isinstance(
-            channel, channel_models.PermissibleGuildChannel
-        ), "CHANNEL_CREATE events for threads and DMS are undocumented behaviour"
+        assert isinstance(channel, channel_models.PermissibleGuildChannel), (
+            "CHANNEL_CREATE events for threads and DMS are undocumented behaviour"
+        )
         return channel_events.GuildChannelCreateEvent(shard=shard, channel=channel)
 
     def deserialize_guild_channel_update_event(
@@ -109,18 +109,18 @@ def deserialize_guild_channel_update_event(
         old_channel: typing.Optional[channel_models.PermissibleGuildChannel] = None,
     ) -> channel_events.GuildChannelUpdateEvent:
         channel = self._app.entity_factory.deserialize_channel(payload)
-        assert isinstance(
-            channel, channel_models.PermissibleGuildChannel
-        ), "CHANNEL_UPDATE events for threads and DMS are undocumented behaviour"
+        assert isinstance(channel, channel_models.PermissibleGuildChannel), (
+            "CHANNEL_UPDATE events for threads and DMS are undocumented behaviour"
+        )
         return channel_events.GuildChannelUpdateEvent(shard=shard, channel=channel, old_channel=old_channel)
 
     def deserialize_guild_channel_delete_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
     ) -> channel_events.GuildChannelDeleteEvent:
         channel = self._app.entity_factory.deserialize_channel(payload)
-        assert isinstance(
-            channel, channel_models.PermissibleGuildChannel
-        ), "CHANNEL_DELETE events for threads and DMS are undocumented behaviour"
+        assert isinstance(channel, channel_models.PermissibleGuildChannel), (
+            "CHANNEL_DELETE events for threads and DMS are undocumented behaviour"
+        )
         return channel_events.GuildChannelDeleteEvent(shard=shard, channel=channel)
 
     def deserialize_channel_pins_update_event(
diff --git a/pipelines/nox.py b/pipelines/nox.py
index 891d12af..445e99c0 100644
--- a/pipelines/nox.py
+++ b/pipelines/nox.py
@@ -32,19 +32,9 @@
 
 from pipelines import config as _pipelines_config
 
-try:
-    import uv
-
-    del uv
-
-    venv_backend = "uv"
-except ModuleNotFoundError:
-    venv_backend = "venv"
-
-
 # Default sessions should be defined here
 _options.sessions = ["reformat-code", "codespell", "pytest", "flake8", "slotscheck", "mypy", "verify-types"]
-_options.default_venv_backend = venv_backend
+_options.default_venv_backend = "uv|venv"
 
 _NoxCallbackSig = _typing.Callable[[Session], None]
 
diff --git a/requirements.txt b/requirements.txt
index 2bfa8bf4..7d436865 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
 aiohttp~=3.11
-attrs~=24.3
+attrs~=25.1
 colorlog~=6.9
 multidict~=6.1


Test patch:
diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt
index d1c453eb..cc3e55b3 100644
--- a/dev-requirements/pytest.txt
+++ b/dev-requirements/pytest.txt
@@ -2,7 +2,7 @@
 mock==5.1.0
 
 pytest==8.3.4
-pytest-asyncio==0.25.0
+pytest-asyncio==0.25.3
 pytest-cov==6.0.0
 pytest-randomly==3.16.0
 
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index d89ba045..67d116be 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -7169,6 +7169,21 @@ def entitlement_payload(self):
             "subscription_id": "1019653835926409216",
         }
 
+    @pytest.fixture
+    def entitlement_payload_starts_ends_null(self):
+        return {
+            "id": "696969696969696",
+            "sku_id": "420420420420420",
+            "application_id": "123123123123123",
+            "type": 8,
+            "deleted": False,
+            "starts_at": None,
+            "ends_at": None,
+            "guild_id": "1015034326372454400",
+            "user_id": "115590097100865541",
+            "subscription_id": "1019653835926409216",
+        }
+
     @pytest.fixture
     def sku_payload(self):
         return {
@@ -7195,6 +7210,21 @@ def test_deserialize_entitlement(self, entity_factory_impl, entitlement_payload)
         assert entitlement.subscription_id == 1019653835926409216
         assert isinstance(entitlement, monetization_models.Entitlement)
 
+    def test_deserialize_entitlement_starts_ends_null(self, entity_factory_impl, entitlement_payload_starts_ends_null):
+        entitlement = entity_factory_impl.deserialize_entitlement(entitlement_payload_starts_ends_null)
+
+        assert entitlement.id == 696969696969696
+        assert entitlement.sku_id == 420420420420420
+        assert entitlement.application_id == 123123123123123
+        assert entitlement.type is monetization_models.EntitlementType.APPLICATION_SUBSCRIPTION
+        assert entitlement.is_deleted is False
+        assert entitlement.starts_at is None
+        assert entitlement.ends_at is None
+        assert entitlement.guild_id == 1015034326372454400
+        assert entitlement.user_id == 115590097100865541
+        assert entitlement.subscription_id == 1019653835926409216
+        assert isinstance(entitlement, monetization_models.Entitlement)
+
     def test_deserialize_sku(self, entity_factory_impl, sku_payload):
         sku = entity_factory_impl.deserialize_sku(sku_payload)
 
diff --git a/tests/hikari/internal/test_aio.py b/tests/hikari/internal/test_aio.py
index 6cbf042f..c5c29895 100644
--- a/tests/hikari/internal/test_aio.py
+++ b/tests/hikari/internal/test_aio.py
@@ -302,6 +302,9 @@ def test_get_or_make_loop():
 
     assert aio.get_or_make_loop() is mock_loop
 
+    # Make sure to cleanup event loop or pytest_asyncio will error
+    asyncio.set_event_loop(None)
+
 
 def test_get_or_make_loop_handles_runtime_error():
     asyncio.set_event_loop(None)